Никак не могу оставить в прошлом, одну историю, произошедшую со мной больше 7 лет назад.
На тот момент я, еще студент последнего курса универа, только получил свою первую работу в IT... Как сейчас помню свои эмоции. Наконец‑то, спустя годы подготовок и отказов, получаешь свой первый «настоящий» проект. Осмотревшись по сторонам, понимаю, что кругом меня не то что других джунов нет, но даже мидлов. Сплошные синьоры и лиды, как тогда казалось — грозные дядьки, с большим опытом... Ну ничего, сейчас я им покажу, что такое «молодая гвардия» ?.
Получаю компьютер, креды для доступа, мне подробнее рассказывают про проект, присылают ссылки на минимальный набор сервисов, что нужно будет локально поднять для работы и отправляют настраивать окружение. В первый же день я сломал заботливо предустановленную мне убунту ? (удалил «не ту» версию питона, которая, как выяснилась, очень нужна), ну да ладно, мелочи, с кем не бывает?
Установил минт, начал настраивать иде, окружение, забрал себе нужные сервисы, вроде все хорошо, НО в одном из сервисов стабильно падает один и тот же тест. Запускаю отдельно — все хорошо и стабильно. Запускаю через сборщик (mvn test) — падение. Пытаюсь разобраться, что происходит — ничего не понятно. Тест падает из‑за мока, которого вообще нет в этом тестовом сценарии. Больше того, смущает ситуация, что ни на ci, ни у кого из коллег такого не происходит. Тест стабилен, да и в нем не меняли ничего уже довольно давно. Вывод: проблема на моей стороне и разбираться мне с ней самому.
Пытаюсь откопать, от куда этот мок мог вообще попасть, и сталкиваюсь с суровой реальностью: количество тестов измеряется тысячами, этот мок копипастом использовался во многих из них... Да и не должны, в конце концов моки из одних тестовых сценариев влиять на другие... Ситуация, на тот момент, мне казалось безнадежной. Уже хотел @Ignore повесить над тестом и запомнить, что вот этот тест нужно каждый раз в ручную запускать (не помогло. Начал падать следующий тест, а за ним еще один, все явно по той причине). Ладно, оставил как есть. Просто будем локально игнорировать ошибку, пока не разберусь в чем дело.
Примерно 2 недели... Тот срок, за который я, по мимо своих полученных задач пытался разобраться, что же происходит. А происходило следующее:
примерно пол года назад проводился небольшой рефакторинг
в рамках этого рефакторинга, один из тестируемых классов перестал напрямую использовать эту утилитную функцию
благодаря тому, что мок по факту не использовался в тестовом классе - он не попал в автоматический ресет и просочился в следующие тесты
по некой случайности (ну, или другие места с этой проблемой были исправлены ранее), в порядке запуска тестов, это оказался последний класс, на который этот мок мог повлиять. И, соответственно, все тесты у коллег и на ci проходили.
при настройке нового окружения, я использовал новую версию мавена (естественно, уже не вспомню, о какой версии речь, но не суть). Отличалась PATCH версия, но этого оказалось достаточно, чтоб запустить тесты в другом порядке и ошибка, СПУСТЯ ПОЛ ГОДА, начала проявляться.
Наблюдая дальше, как на этом проекте, так и на других проектах, в других компаниях, которые писали совсем другие люди, я вижу одну и ту же закономерность: моки, из «друзей», с которыми так легко тестировать код, с ростом проекта и сложности стабильно превращаются во «врагов». То что в моменте позволило нам легко проверить, что какой‑то внешний код вызвался с нужными параметрами, что на определенный ответ наша функция повела себя правильно, стабильно приводит к настолько сложным конфигурациям теста, что по ним не то что сложно понять «а что вообще происходит?», но их порой проще полностью переписать, после очередных изменений, чем заставить работать. Минимальные изменения кода приводят к необходимости обновлять кучи тестов. И ведь это синдром.
Тесты, по своей сути, должны быть простым подтверждением, что наш код работает ��ак, как мы от него ожидаем. Но вместо этого, он становится сложнее, запутаннее, связаннее, чем код, который он должен тестировать. Это не нормально, что вместо того, чтоб посмотреть на тест и понять, как работает код, мы чаще смотрим на код, чтоб понять, как работает тест. Сложность их написания и поддержки на столько большая, что разработчики стараются саботировать тестовое покрытие, оставляя тесты только «на действительно важных» компонентах, а рассказ, что у нас 95+% тестовое покрытие вызывает либо недоверие, либо сочувствие у других разработчиков.
Но ведь мы — разработчики: мы учились писать хороший, поддерживаемый код, который не должен доставлять столько боли. Почему мы не можем справиться с какими-то тестами? Я утверждаю, что проблема в моках. То как мы вынуждены их писать, с ними работать провоцирует нас делать строгое разделение: вот тут у нас наши исходники, где мы стараемся писать чисто, красиво и аккуратно, а вот тут тесты, к которым стандартные приемы не применимы. Тут мы легко занимаемся копипастом, потому что "ну не переписывать же эту портянку из when...thenReturn", тут мы легко делаем бешенную связанность и завязываем наш код на внутреннее состояние, ведь по другому они не конфигруируются и т.д. Этот отказ от стандартных практик и провоцирует нас еще больше захламлять наши тесты и делать их все меньше пригодными для нас самих.
Меньше слов, больше кода?
Вот классический пример теста для проверки применения скидки при заказе:
class DiscountServiceTest {
@Mock private lateinit var userRepository: UserRepository
@Mock private lateinit var productRepository: ProductRepository
@Mock private lateinit var promoService: PromoService
@InjectMock private lateinit var discountService: DiscountService
@Test
fun `should apply promo code`() {
// Arrange
val userId = 1L
val productId = 2L
val promoCode = Promo("SALE20")
val mockUser = mock<User> {
on { id } doReturn userId
on { status } doReturn UserStatus.ACTIVE
}
val mockProduct = mock<Product> {
on { id } doReturn productId
on { price } doReturn Money(1000.0)
on { category } doReturn Category.ELECTRONICS
}
`when`(userRepository.findById(userId)).thenReturn(mockUser)
`when`(productRepository.findById(productId)).thenReturn(mockProduct)
`when`(promoService.validatePromo(promoCode, Category.ELECTRONICS)).thenReturn(true)
`when`(promoService.calculateDiscount(promoCode, Money(1000.0))).thenReturn(Money(200.0))
// Act
val result = discountService.calculateFinalPrice(userId, productId, promoCode)
// Assert
verify(userRepository).findById(userId)
verify(productRepository).findById(productId)
verify(promoService).validatePromo(promoCode, Category.ELECTRONICS)
verify(promoService).calculateDiscount(promoCode, Money(1000.0))
assertEquals(Money(800.0), result) // 1000 - 20% promo
}
}
Мне одному кажется, что вместо того, что тест проверяет скорее свою собственную конфигурацию, чем поведение системы? Зачем-то знает о внутреннем состоянии, какие методы других сервисов и репозиториев вызываются. А как только хоть что-то изменится - наши тесты резко перестанут работать... А теперь представьте, сколько мусора было бы здесь с усложнением логики и как часто приходилось бы его обновлять по любым причинам?
Можно с этим что-то сделать? ДА. И все эти методы, не какое-то ноу-хау, а то, что все давно знали, но незаслуженно забыли (или отказываются применять к юнит и интеграционным тестам). Старые добрые фикстуры, стабы, фейки... Да-да, требуют гораздо больше времени на старте. На них не получится просто пове��ить аннотацию, и чтоб все заработало. Но какой результат?
class DiscountServiceTest {
private lateinit var fakeUserRepository: FakeUserRepository
private lateinit var fakeProductRepository: FakeProductRepository
private lateinit var fakePromoService: FakePromoService
private lateinit var discountService: DiscountService
@BeforeEach
fun setUp() {
fakeUserRepository = FakeUserRepository()
fakeProductRepository = FakeProductRepository()
fakePromoService = FakePromoService()
discountService = DiscountService(
fakeUserRepository,
fakeProductRepository,
fakePromoService
)
}
@Test
fun `should apply promo code to expensive electronics`() {
// Arrange
val user = Fixtures.createUser()
val expensiveProduct = Fixtures.createProduct(
price = 1000.USD,
category = Category.ELECTRONICS
)
val promoCode = Fixtures.createPromocode()
fakeUserRepository.givenUserExists(user)
fakeProductRepository.givenProductExists(expensiveProduct)
fakePromoService.givenPromoValid(
code = promoCode,
config = Fixtures.createPromoConfig(
value = 20.0.percent,
category = Category.ELECTRONICS
)
)
// Act
val result = discountService.calculateFinalPrice(
userId = user.id,
productId = expensiveProduct.id,
promoCode = promoCode
)
// Assert
assertThat(result).isEqualTo(
ExpectedPrice.finalPrice(
originalPrice = 1000.USD,
finalPrice = 800.USD // 1000 - 20%
)
)
}
@Test
fun `should not apply promo for invalid category`() {
// Arrange
val user = Fixtures.createUser()
val foodProduct = Fixtures.createProduct(
category = Category.FOOD // Промокод не применяется к еде
)
val promoCode = Fixtures.createPromocode()
fakeUserRepository.givenUserExists(user)
fakeProductRepository.givenProductExists(foodProduct)
fakePromoService.givenPromoValid(
code = promoCode,
config = Fixtures.createPromoConfig(
category = Category.ELECTRONICS
)
)
// Act
val result = discountService.calculateFinalPrice(
userId = user.id,
productId = foodProduct.id,
promoCode = promoCode
)
// Assert
assertThat(result).isEqualTo(
ExpectedPrice.finalPrice(
originalPrice = foodProduct.price,
finalPrice = foodProduct.price // цена без изменений
)
)
}
}
Кажется, что почти ничего не изменилось. Но так ли это? Теперь это чистый код на котлине (ничего не мешает сделать то же самое на джаве или любом другом языке с билдерами, если нет поддержки именованных опциональных параметров). Его можно легко продебажить, если что-то вдруг пошло не так. Он не знает, какие именно методы использует calculateFinalPrice. Мы просто декларативно указываем, что хотим получить, и оно работает. А самое прекрасное, что никакие изменения вроде использования других методов сервисов или репозиториев, добавление каких-то полей в любую из сущностей, которая как либо используется, изменение внутренней логики НЕ ВЛИЯЮТ на тест, пока что-то в действительности не сломается. Нам больше не нужно видеть МР-ы, в которых мы добавляем одно поле, и сотни изменений в тестах из-за него. И тесты действительно независимы друг от друга и ни при каких обстоятельствах не будут делать ничего, что мы от них не ожидаем.
И согласен, чтоб все это хорошо и красиво работало, требуется привести в порядок и кодовую базу. Но об этом, в следующей части.
Комментарии (20)

boulder
25.11.2025 06:46Меньше слов, больше кода?
Да уж лучше наоборот :)
Если тесты однотипные, то проще один раз хорошенько подумать, создать специализированный формат и задавать тесты, скажем, как: {in: [1, 2, 3], out:[10, 20, 30]}, а не кодом на два экрана (каждый!). Это же совершенно нечитаемо и немодернизируемо (
simarel Автор
25.11.2025 06:46Параметризированные тесты не вчера придуманы. Я... Как-то надеялся по верхам быстрее проскочить к чему то более интересному, а не дублировать то, что и так легко ищется по первой вкладке гугла

boulder
25.11.2025 06:46Вы в статье или в описанной работе хотели сразу "проскочить к ... интересному"? :)

simarel Автор
25.11.2025 06:46В следующих частях)
Добавить параметры к тесту - это уже больше чем-то на учебник для самых маленьких похоже, а не на сборник статей, где я пытаюсь систематизировать свой опыт и идеи

aleksandy
25.11.2025 06:46С самого начала было понятно, что проблема возникает из-за порядка выполнения тестов. А вот это стало возможно в принципе потому, что инструмент используется, а пользоваться им правильно никто не удосужился (не "не умеет"!!!). Подобные проблемы решаются простым
@AfterEach-методом, в котором все моки передаются вverifyNoMoreInteractions(). Опционально, можно добавить@BeforeEachсreset()-ом.Всё, проблема финито. Если в каком-то тесте вызов мока не провалидирован - это херовый тест. Потому что он завязан (пусть даже неявно) на мок, но эту завязку не проверяет.
Ну, и вдобавок каждый тест с
@Mock-ом - это отдельный спринговый контекст, который надо поднять. А это время, и, порой, немалое. Поэтому все моки, если они вообще нужны, должны быть определены в одном базовом тестовом классе, от которого будут наследоваться другие. Иначе на 2 секунды тестов, по 10 секунд на запуск контекстов будет уходить.
simarel Автор
25.11.2025 06:46Не все проблемы. Только с просачиванием моков. Который, ещё раз, нужно везде написать...
Это было скорее введение, с которого началось наблюдение, а не единственная проблема. Проблема же, что они создают очень хрупкую тестовую систему основную скорее на вайтбоксе. Т.е. больше часть теста, на самом деле, не проверка выполнения, а проверка, что моки правильно сконфигурированы.
Опять же, вот эти перезапуски в разных контекстах, моки в бейс классе, обязательный afterEach/beforeEach... это точно показатель, что с тестами все в порядке? Выглядит как набор костылей, по которому нужно пройтись, чтоб это все более менее адекватно работало (нет), а не как надёжная адекватная система as-is

aleksandy
25.11.2025 06:46Проблема же, что они создают очень хрупкую тестовую систему основную скорее на вайтбоксе. Т.е. больше часть теста, на самом деле, не проверка выполнения, а проверка, что моки правильно сконфигурированы.
А фейковые заглушки, предлагаемые вами в статье, не на этом основаны? Просто моки более настраиваемы, могут быть переиспользованы в разных сценариях. И, опять же, использование моков должно иметь свои пределы.
val mockUser = mock<User> { on { id } doReturn userId on { status } doReturn UserStatus.ACTIVE } val mockProduct = mock<Product> { on { id } doReturn productId on { price } doReturn Money(1000.0) on { category } doReturn Category.ELECTRONICS }Заставь дурака богу молиться, он и лоб расшибёт. (c) За такой "мокизм головного мозга" надо железной линейкой по пальцам. Почему эти объекты просто не создать через
newс нужными значениями атрибутов?`when`(userRepository.findById(userId)).thenReturn(mockUser) `when`(productRepository.findById(productId)).thenReturn(mockProduct) `when`(promoService.validatePromo(promoCode, Category.ELECTRONICS)).thenReturn(true) `when`(promoService.calculateDiscount(promoCode, Money(1000.0))).thenReturn(Money(200.0))Наличие подобного - признак хренового теста. Моки в тестах должны принадлежать к одному слоою. А тут, очевидно, и слой хранения, и слой бизнес-логики. Т.е. это тест ради теста, а не для проверки функционала. Такое имеет право на существование исключительно редких случаях легаси-проектов, когда включают проверки на минимальное покрытие тестами, а тестов нет вообще. Но это техдолг с рождения, который должен быть переписан.
Уж лучше никаких тестов, чем такие. Так хотя бы не появляется иллюзии, что ты что-то контролируешь.
обязательный afterEach/beforeEach
Достаточно сделать их в базовом классе.
Выглядит как набор костылей,
Моки - это в принципе костыли для тестов.

simarel Автор
25.11.2025 06:46А фейковые заглушки, предлагаемые вами в статье, не на этом основаны?
Нет. Я изначально надеялся только зафиксировать идею, но видимо придется показывать в отдельной части...
Идея в том, что фейк является полноценной (пусть и очень простой) имплементацией. И тогда получаем систему, в которойЕсли вдруг изменился вызываемый метод того же класса (который уже есть в интерфейсе), то фейк продолжит работать.
Если изменилась сигнатура метода - упадет билд с явной ошибкой, которую нужно поправить в 1 месте - фейке и она продолжит работать со всеми тестами.
Если изменился сам используемый класс - опять же, это явно можно будет увидеть падающим билдом с нормальной ошибкой
Да, при этом возникает вопрос, что "не хочу я какой нибудь репозиторий реализовывать со всеми его методами"... Но оно и не надо. Гранулярность интерфейсов - штука достаточно важная и ее можно поддерживать.
Почему эти объекты просто не создать через
newс нужными значениями атрибутов?Отличный вопрос. В целом, фикстура так и делает. Но в одном месте и с набором дефолтных параметров. Соответственно, когда у тебя изменяются параметры в этих объектах, нам нужно исправить это в самой фикстуре + в тех тестах, где они явно использовались, а не лазать по множеству мест копипастом расставляя параметры, которые вообще не значимы для этого теста и получать мр-ы, где реальных изменений на пару десяток строк, и сотни изменений в тестах, с параметрами.
Здесь в моке попытка в похожий подход, чтоб не приходилось изменять это лишний раз. И опять же, объект может быть достаточно сложный, с множеством параметров и целой простыней кода, которые нужно для этого написать. А потом такой объект становится не 1.
Моки в тестах должны принадлежать к одному слоою. А тут, очевидно, и слой хранения, и слой бизнес-логики.
Хороший вертикальный слайсинг в рамках фичи - конечно, штука хорошая... Вот только на практике, почти невозможная, когда логика становится сложнее чем круд. Да, в теории, можно собрать слой абстракции наверху, который будет "более правильно" координировать эти задачки, чтоб запросы аккуратно и красиво шли только в следующий слой... И понадеется, что нам не понадобится через какое-то время еще слой абстракции, чтоб как-то запросы в этот слой координировать и не заниматься при этом копипастой... Ну, или у нас юнит тест становится не юнитом, а пол слоя тестирует...
Моки - это в принципе костыли для тестов.
Это конечно костыль, но это наш костыль... И в целом то, ничего в этом сильно плохого нет... Пока такие костыльные решения не называют нормой.
Да, вариант с фейками более развесистый, там придется много кода своего писать, а не навешать аноташку. Соответственно, на старте это дольше. Там какие-то свои имплементации, в которых тоже баги могут быть. Да, это не привычно. Но, набор проблем он не из воздуха взят, как и набор костылей (и надежд), чтоб это все таки как-то заработало. Но на сколько бы спорным не был описанный вариант, он своеобразно, но тем не менее, пытается их решить (и на мой взгляд, у него не плохо получается)
aleksandy
25.11.2025 06:46Идея в том, что фейк является полноценной (пусть и очень простой) имплементацией.
Если вдруг изменился вызываемый метод того же класса (который уже есть в интерфейсе), то фейк продолжит работать.
Если изменилась сигнатура метода - упадет билд с явной ошибкой, которую нужно поправить в 1 месте - фейке и она продолжит работать со всеми тестами.
Всё ровно так, как и в ситуации с моком.
юнит тест становится не юнитом
Юнит-тест и спринговый контекст - два непересекающихся множества, если новый контекст не поднимается на каждый тестовый метод. Но если так делать, то даже на средних проектах тесты будут выполняться до морковкиного заговения. Так никто в здравом уме делать не будет.
а пол слоя тестирует...
А вот это, конечно, плохо, очень плохо. Тестировать надо всё, а не половину.
у него не плохо получается)
Ничем не лучше, но и не хуже моков.

simarel Автор
25.11.2025 06:46Идея в том, что фейк является полноценной (пусть и очень простой) имплементацией.
Если вдруг изменился вызываемый метод того же класса (который уже есть в интерфейсе), то фейк продолжит работать.
Если изменилась сигнатура метода - упадет билд с явной ошибкой, которую нужно поправить в 1 месте - фейке и она продолжит работать со всеми тестами.
Всё ровно так, как и в ситуации с моком.
Ну как продолжит? В моке нужно явно указать сигнатуру вызываемого метода и поддерживать его на постоянной основе. И да, опять же, некоторые (!) мок фреймворки, или если разобраться и их правильно сконфигурировать, хотя бы честно скажут, что вот тут ожидался вызов этого и не вызвался, а вот тут попытались вызвать вот это, но мок не сконфигурирован... А есть, например, mockito, которая будет возвращать дефолтный респонс. Да, где-то там, ниже, у тебя может быть проверка, что каждый мок вызван, столько-то раз, с такими-то аргументами (опять же, не всегда), но чаще там где-то null залетит, как-то пролетит через пол теста, а потом вдруг NPE, или какая-то другая ошибка из-за этого null. И иди ищи от куда он взялся. Не используется половина моков? Ну и ладно, бывает. А у тебя это мертвый код, который усложняет восприятие этого теста... Да, можно донастроить, учитывать нюансы, но какого черта об этом нужно думать? В больших проектах и так большая сложность. От лишней когнитивной нагрузки нужно избавляться, а не пытаться ее нормализовывать.
Юнит-тест и спринговый контекст...
А кто говорил о поднятии всего спрингового контекста?
Тестировать надо всё, а не половину.
В норме, количество юнит тестов, для которых этот контекст не нужен - огромно. Интеграционных, где можно говорить о тестировании более значительного объема кодобазы - гораздо меньше.

HardlinePeak936
25.11.2025 06:46В теме статьи не разбираюсь, но вот за твоё поведение во времена стажировки из-за этих «моков»... Однозначно нужно дать подзатыльник. Не забыв пояснить, что любые проблемы нужно решать, если возможно, либо не маскировать, прятать, умалчивать и откладывать, а сразу идти дёргать наставника, руководителя, дядю директора либо, хотя бы (!), соседа-нескольких. А-та-та, надеюсь уже исправились к этому времени ;)

simarel Автор
25.11.2025 06:46Да щас, дёргал я. Ответ был один: разбирайся. Тогда я этот подход не понял, но потом заметил этот паттерн и дальше...
- А что будет, если...
- Пробуй
- А если...
- Пробуй
- Тут какая-то фигня, не понятно
- Разбирайся
Естественно, речь не шла о таком походе, когда запара была серьёзная: лежит прод, клиенты жалуются итд...
Но ты раз что-то предложил, попробовал, не получилось. Второй раз. Третий раз... Зато ты не боишься этого. Не учишься ни "ваше мнение тут никого не интересует" ни "инициатива сношает инициатора". Нет, ты попробовал, получил какой-то результат, понял, идём дальше. Никто в тебя пальцем не тыкает... Что-то тут не понятное - идём и разбираемся.
И по итогу, когда через года ты находишься перед выбором "а что если попробовать...", когда у тебя по факту может получиться, когда эти решения, собранные из идеи на коленке за несколько часов может решить серьёзные проблемы, позволяя бизнесу двигаться дальше - у тебя не будет вопроса "стоит ли пробовать?", при том, что может там придётся потыкаться и ни к чему не придти по итогу или лучше заткнуться и сидеть не высовываться.

cupraer
25.11.2025 06:46Часть 2. Колбэки — это технический долг.
Часть 3. Передача управления — это технический долг.
Часть 4. Многопоточность — это технический долг.
Часть 5. Конечные автоматы — это технический долг.
Часть 6. Логическое ветвление — это технический долг.
…Ваши попытки разобраться затянулись. 2 недели — это нормально. 7 лет — это перебор. Найдите умного дяденьку и спросите, как работать с моками.
А то через некоторое время вы столкнетесь с тестированием сложного многопоточного кода, где без моков — практически никак, и будет больно.
benjik
Судя по статье, не "Моки - это технический долг", а "Кривые моки и грязный тестовый код - это технический долг".
В первом примере у вас тесты с моками влезли на один экран. Во втором - на два, и это без определения всех зависимостей FakeЧегоТоТам, которые ещё пару экранов займут.
Сколько времени займёт "причёсывание" одного экрана с моками и четырёх экранов с самописными фейками при необходимости? Как быстро самописные фейки превратятся в полу-универсальные и кривые самописные моки, когда тестов прибавится?
Все вот эти
можно достичь теми же моками, с меньшим количеством строк, просто уделяя какое-то время дизайну тестового кода. Безумный копипаст так же точно угробит FakeЧегоТоТам, только будет ещё больше грязи.
simarel Автор
Да, но там и 2 теста, когда в 1 примере лишь один. Согласен, фейки, фикстуры ещё нужно написать, это тоже занимает место. Да, больше кода. Больше адекватного привычного нам кода который ведёт себя так, как от него это ожидают.
В чем-то согласен. Проблем действительно можно избежать "если делать все правильно", "хорошо прочитать доку (и релиз ноутсы на каждый апдейт) и нигде ничего не пропустить" и вообще "просто уделяя какое-то время дизайну тестового кода". Вот только "нигде ничего не забыть" легко пропустить, а исправлять ситуацию не так тривиально. Ещё видел примеры, где по аналогии с фикстурами пытались делать человеческие конфигурации моков... Но где-то кроме пары демонстрационных примеров в сети я этого не видел. И ключевая проблема: как не пытайся настраивать моки, с ними нельзя работать как с чёрным ящиком. Нет, укажи конкретно, на какую сигнатуру запроса ты хочешь свой ответ. И если где-то меняется хоть что-то (от используемого метода до некоторого дополнительного параметра), вместо того, чтоб исправить это при необходимости в одном месте, в случае изменения сигнатуры, и написать один тест, который это проверяет, мы должны пройти по всем тестам, где он используется в попытке все исправить (что далеко не всегда нужно)
Никто ведь и не спорит. Моки на старте, в режиме "один раз написал, забыл и надеешься, что тебе не придётся в этом разбираться" быстрее. А вот с поддержкой... Я бы не сказал. С ростом сложности и моков, поддерживать это куда сложнее, в то время как в фейках большая часть работы легко выносится в довольно простой дженерик, и из собственной реализации только матчеры (при необходимости) или простые и говорящие верифай методы. Там просто нечему становиться сложным