Comments 30
Судя по статье, не "Моки - это технический долг", а "Кривые моки и грязный тестовый код - это технический долг".
В первом примере у вас тесты с моками влезли на один экран. Во втором - на два, и это без определения всех зависимостей FakeЧегоТоТам, которые ещё пару экранов займут.
Сколько времени займёт "причёсывание" одного экрана с моками и четырёх экранов с самописными фейками при необходимости? Как быстро самописные фейки превратятся в полу-универсальные и кривые самописные моки, когда тестов прибавится?
Все вот эти
Его можно легко продебажить
Мы просто декларативно указываем, что хотим получить, и оно работает
никакие изменения ... НЕ ВЛИЯЮТ на тест, пока что-то в действительности не сломается
можно достичь теми же моками, с меньшим количеством строк, просто уделяя какое-то время дизайну тестового кода. Безумный копипаст так же точно угробит FakeЧегоТоТам, только будет ещё больше грязи.
В первом примере у вас тесты с моками влезли на один экран. Во втором - на два, и это без определения всех зависимостей
Да, но там и 2 теста, когда в 1 примере лишь один. Согласен, фейки, фикстуры ещё нужно написать, это тоже занимает место. Да, больше кода. Больше адекватного привычного нам кода который ведёт себя так, как от него это ожидают.
Кривые моки и грязный тестовый код - это технический долг.
В чем-то согласен. Проблем действительно можно избежать "если делать все правильно", "хорошо прочитать доку (и релиз ноутсы на каждый апдейт) и нигде ничего не пропустить" и вообще "просто уделяя какое-то время дизайну тестового кода". Вот только "нигде ничего не забыть" легко пропустить, а исправлять ситуацию не так тривиально. Ещё видел примеры, где по аналогии с фикстурами пытались делать человеческие конфигурации моков... Но где-то кроме пары демонстрационных примеров в сети я этого не видел. И ключевая проблема: как не пытайся настраивать моки, с ними нельзя работать как с чёрным ящиком. Нет, укажи конкретно, на какую сигнатуру запроса ты хочешь свой ответ. И если где-то меняется хоть что-то (от используемого метода до некоторого дополнительного параметра), вместо того, чтоб исправить это при необходимости в одном месте, в случае изменения сигнатуры, и написать один тест, который это проверяет, мы должны пройти по всем тестам, где он используется в попытке все исправить (что далеко не всегда нужно)
Сколько времени займёт "причёсывание" одного экрана с моками и четырёх экранов с самописными фейками при необходимости?
Никто ведь и не спорит. Моки на старте, в режиме "один раз написал, забыл и надеешься, что тебе не придётся в этом разбираться" быстрее. А вот с поддержкой... Я бы не сказал. С ростом сложности и моков, поддерживать это куда сложнее, в то время как в фейках большая часть работы легко выносится в довольно простой дженерик, и из собственной реализации только матчеры (при необходимости) или простые и говорящие верифай методы. Там просто нечему становиться сложным
Меньше слов, больше кода?
Да уж лучше наоборот :)
Если тесты однотипные, то проще один раз хорошенько подумать, создать специализированный формат и задавать тесты, скажем, как: {in: [1, 2, 3], out:[10, 20, 30]}, а не кодом на два экрана (каждый!). Это же совершенно нечитаемо и немодернизируемо (
С самого начала было понятно, что проблема возникает из-за порядка выполнения тестов. А вот это стало возможно в принципе потому, что инструмент используется, а пользоваться им правильно никто не удосужился (не "не умеет"!!!). Подобные проблемы решаются простым @AfterEach-методом, в котором все моки передаются в verifyNoMoreInteractions(). Опционально, можно добавить @BeforeEach с reset()-ом.
Всё, проблема финито. Если в каком-то тесте вызов мока не провалидирован - это херовый тест. Потому что он завязан (пусть даже неявно) на мок, но эту завязку не проверяет.
Ну, и вдобавок каждый тест с @Mock-ом - это отдельный спринговый контекст, который надо поднять. А это время, и, порой, немалое. Поэтому все моки, если они вообще нужны, должны быть определены в одном базовом тестовом классе, от которого будут наследоваться другие. Иначе на 2 секунды тестов, по 10 секунд на запуск контекстов будет уходить.
Не все проблемы. Только с просачиванием моков. Который, ещё раз, нужно везде написать...
Это было скорее введение, с которого началось наблюдение, а не единственная проблема. Проблема же, что они создают очень хрупкую тестовую систему основную скорее на вайтбоксе. Т.е. больше часть теста, на самом деле, не проверка выполнения, а проверка, что моки правильно сконфигурированы.
Опять же, вот эти перезапуски в разных контекстах, моки в бейс классе, обязательный afterEach/beforeEach... это точно показатель, что с тестами все в порядке? Выглядит как набор костылей, по которому нужно пройтись, чтоб это все более менее адекватно работало (нет), а не как надёжная адекватная система as-is
Проблема же, что они создают очень хрупкую тестовую систему основную скорее на вайтбоксе. Т.е. больше часть теста, на самом деле, не проверка выполнения, а проверка, что моки правильно сконфигурированы.
А фейковые заглушки, предлагаемые вами в статье, не на этом основаны? Просто моки более настраиваемы, могут быть переиспользованы в разных сценариях. И, опять же, использование моков должно иметь свои пределы.
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
Достаточно сделать их в базовом классе.
Выглядит как набор костылей,
Моки - это в принципе костыли для тестов.
А фейковые заглушки, предлагаемые вами в статье, не на этом основаны?
Нет. Я изначально надеялся только зафиксировать идею, но видимо придется показывать в отдельной части...
Идея в том, что фейк является полноценной (пусть и очень простой) имплементацией. И тогда получаем систему, в которой
Если вдруг изменился вызываемый метод того же класса (который уже есть в интерфейсе), то фейк продолжит работать.
Если изменилась сигнатура метода - упадет билд с явной ошибкой, которую нужно поправить в 1 месте - фейке и она продолжит работать со всеми тестами.
Если изменился сам используемый класс - опять же, это явно можно будет увидеть падающим билдом с нормальной ошибкой
Да, при этом возникает вопрос, что "не хочу я какой нибудь репозиторий реализовывать со всеми его методами"... Но оно и не надо. Гранулярность интерфейсов - штука достаточно важная и ее можно поддерживать.
Почему эти объекты просто не создать через
newс нужными значениями атрибутов?
Отличный вопрос. В целом, фикстура так и делает. Но в одном месте и с набором дефолтных параметров. Соответственно, когда у тебя изменяются параметры в этих объектах, нам нужно исправить это в самой фикстуре + в тех тестах, где они явно использовались, а не лазать по множеству мест копипастом расставляя параметры, которые вообще не значимы для этого теста и получать мр-ы, где реальных изменений на пару десяток строк, и сотни изменений в тестах, с параметрами.
Здесь в моке попытка в похожий подход, чтоб не приходилось изменять это лишний раз. И опять же, объект может быть достаточно сложный, с множеством параметров и целой простыней кода, которые нужно для этого написать. А потом такой объект становится не 1.
Моки в тестах должны принадлежать к одному слоою. А тут, очевидно, и слой хранения, и слой бизнес-логики.
Хороший вертикальный слайсинг в рамках фичи - конечно, штука хорошая... Вот только на практике, почти невозможная, когда логика становится сложнее чем круд. Да, в теории, можно собрать слой абстракции наверху, который будет "более правильно" координировать эти задачки, чтоб запросы аккуратно и красиво шли только в следующий слой... И понадеется, что нам не понадобится через какое-то время еще слой абстракции, чтоб как-то запросы в этот слой координировать и не заниматься при этом копипастой... Ну, или у нас юнит тест становится не юнитом, а пол слоя тестирует...
Моки - это в принципе костыли для тестов.
Это конечно костыль, но это наш костыль... И в целом то, ничего в этом сильно плохого нет... Пока такие костыльные решения не называют нормой.
Да, вариант с фейками более развесистый, там придется много кода своего писать, а не навешать аноташку. Соответственно, на старте это дольше. Там какие-то свои имплементации, в которых тоже баги могут быть. Да, это не привычно. Но, набор проблем он не из воздуха взят, как и набор костылей (и надежд), чтоб это все таки как-то заработало. Но на сколько бы спорным не был описанный вариант, он своеобразно, но тем не менее, пытается их решить (и на мой взгляд, у него не плохо получается)
Идея в том, что фейк является полноценной (пусть и очень простой) имплементацией.
Если вдруг изменился вызываемый метод того же класса (который уже есть в интерфейсе), то фейк продолжит работать.
Если изменилась сигнатура метода - упадет билд с явной ошибкой, которую нужно поправить в 1 месте - фейке и она продолжит работать со всеми тестами.
Всё ровно так, как и в ситуации с моком.
юнит тест становится не юнитом
Юнит-тест и спринговый контекст - два непересекающихся множества, если новый контекст не поднимается на каждый тестовый метод. Но если так делать, то даже на средних проектах тесты будут выполняться до морковкиного заговения. Так никто в здравом уме делать не будет.
а пол слоя тестирует...
А вот это, конечно, плохо, очень плохо. Тестировать надо всё, а не половину.
у него не плохо получается)
Ничем не лучше, но и не хуже моков.
Идея в том, что фейк является полноценной (пусть и очень простой) имплементацией.
Если вдруг изменился вызываемый метод того же класса (который уже есть в интерфейсе), то фейк продолжит работать.
Если изменилась сигнатура метода - упадет билд с явной ошибкой, которую нужно поправить в 1 месте - фейке и она продолжит работать со всеми тестами.
Всё ровно так, как и в ситуации с моком.
Ну как продолжит? В моке нужно явно указать сигнатуру вызываемого метода и поддерживать его на постоянной основе. И да, опять же, некоторые (!) мок фреймворки, или если разобраться и их правильно сконфигурировать, хотя бы честно скажут, что вот тут ожидался вызов этого и не вызвался, а вот тут попытались вызвать вот это, но мок не сконфигурирован... А есть, например, mockito, которая будет возвращать дефолтный респонс. Да, где-то там, ниже, у тебя может быть проверка, что каждый мок вызван, столько-то раз, с такими-то аргументами (опять же, не всегда), но чаще там где-то null залетит, как-то пролетит через пол теста, а потом вдруг NPE, или какая-то другая ошибка из-за этого null. И иди ищи от куда он взялся. Не используется половина моков? Ну и ладно, бывает. А у тебя это мертвый код, который усложняет восприятие этого теста... Да, можно донастроить, учитывать нюансы, но какого черта об этом нужно думать? В больших проектах и так большая сложность. От лишней когнитивной нагрузки нужно избавляться, а не пытаться ее нормализовывать.
Юнит-тест и спринговый контекст...
А кто говорил о поднятии всего спрингового контекста?
Тестировать надо всё, а не половину.
В норме, количество юнит тестов, для которых этот контекст не нужен - огромно. Интеграционных, где можно говорить о тестировании более значительного объема кодобазы - гораздо меньше.
В теме статьи не разбираюсь, но вот за твоё поведение во времена стажировки из-за этих «моков»... Однозначно нужно дать подзатыльник. Не забыв пояснить, что любые проблемы нужно решать, если возможно, либо не маскировать, прятать, умалчивать и откладывать, а сразу идти дёргать наставника, руководителя, дядю директора либо, хотя бы (!), соседа-нескольких. А-та-та, надеюсь уже исправились к этому времени ;)
Да щас, дёргал я. Ответ был один: разбирайся. Тогда я этот подход не понял, но потом заметил этот паттерн и дальше...
- А что будет, если...
- Пробуй
- А если...
- Пробуй
- Тут какая-то фигня, не понятно
- Разбирайся
Естественно, речь не шла о таком походе, когда запара была серьёзная: лежит прод, клиенты жалуются итд...
Но ты раз что-то предложил, попробовал, не получилось. Второй раз. Третий раз... Зато ты не боишься этого. Не учишься ни "ваше мнение тут никого не интересует" ни "инициатива сношает инициатора". Нет, ты попробовал, получил какой-то результат, понял, идём дальше. Никто в тебя пальцем не тыкает... Что-то тут не понятное - идём и разбираемся.
И по итогу, когда через года ты находишься перед выбором "а что если попробовать...", когда у тебя по факту может получиться, когда эти решения, собранные из идеи на коленке за несколько часов может решить серьёзные проблемы, позволяя бизнесу двигаться дальше - у тебя не будет вопроса "стоит ли пробовать?", при том, что может там придётся потыкаться и ни к чему не придти по итогу или лучше заткнуться и сидеть не высовываться.
Часть 2. Колбэки — это технический долг.
Часть 3. Передача управления — это технический долг.
Часть 4. Многопоточность — это технический долг.
Часть 5. Конечные автоматы — это технический долг.
Часть 6. Логическое ветвление — это технический долг.
…
Ваши попытки разобраться затянулись. 2 недели — это нормально. 7 лет — это перебор. Найдите умного дяденьку и спросите, как работать с моками.
А то через некоторое время вы столкнетесь с тестированием сложного многопоточного кода, где без моков — практически никак, и будет больно.
🤣.
Даже приятно, что за меня так переживают. Не волнуйтесь, как нибудь справлюсь 🤣🤣🤣
Я переживаю не за вас, а за людей, которым вы льёте в уши это дилетантское говнище.
Может все же будут какие-то замечания по существу?
Из того, что покрыть моками весь мир вокруг — так себе решение, — вы делаете неожиданный и слаборелевантный вывод, будто если покрыть весь мир стабами, то все будет хорошо. Не будет.
Если предметом тестирования конкретно тут является «should apply promo code» — нужно стаббить (или использовать реальные данные из тестовой базы — тут от многих параметров зависит, какой подход предпочтительнее) всё, кроме промосервиса. А промосервис нужно мо́чить, а внутри собственно тестов — проверять, что ① был вызван метод проверки, надо ли применять промо ② затем был (либо не был) вызван метод применения с корректными параметрами ③ ну и всякое такое. Интерфейс, иными словами.
Затем нужно написать юнит-тесты для самого́ промосервиса и удостовериться, что он работает как задумано, и что абстракции не текут (если не получается написать юниты без заведения сущностей «юзер», «продукт», «ворлд» или «God» — текут).
С таким подходом у вас полностью отделена реализация от интерфейса, реализация проверяется юнит-тестами, интерфейс — моками, потому что контракт интерфейса в принципе описывается этими самыми моками, их можно даже из документации нагенерить, или наоборот.
Моки — это инструмент проверки, в отличие от стабов. Вы именно что проверяете поведение при помощи моков. Был ли вызван, сколько раз, с какими параметрами, и так далее. И возвращаете правильный результат, потому что работу замоченного сервиса проверят юниты.
① был вызван метод проверки, надо ли применять промо ② затем был (либо не был) вызван метод применения с корректными параметрами ③ ну и всякое такое.
Т.е. фактически ты не тестируешь код. Ты тестируешь, что ты написал свой метод так, как ты его написал. И надеешься, что никогда не придется здесь хоть что-то менять, потому что вместе с этим сразу нужно переписывать большую часть тестов. Это элемент хрупкости, о которой сказано выше.
И да, не смотря на то, что статья написана именно так, не надо возводить никакое мнение в абсолют. Я не предлагаю сейчас всем придти и выпилить все моки из всех своих рабочих проектов. Моки - стандарт де-факто, спорить с этим глупо. И естественно они повсеместно используются. Цель статьи еще раз подсветить эти проблемы и рассказать об альтернативах. Да, в своеобразной форме. Но именно такая форма может донести смысл, в отличии от попыток подбирать какие-то комформистские нейтральные позиции, постоянные дисклеймеры, что думайте своей головой и прочее.
Моки - это просто симптом, а не проблема. Как мне видится, основная проблема - это плохое определение границы модулей, которые мы тестируем в модульном тестировании. Почему-то кто-то решил, а все остальные поверили, что тестовый и продуктовый код должны соответствовать друг другу и что каждый класс нужно тестировать в изоляции. Тестировать класс в изоляции не имеет смысла - имеет смысл тестировать поведение в изоляции. В примере DiscountService и PromoService описывают одну логику/поведение - как считать скидку, и поведение которое мы тестируем - это скидка при наличии промокода (а не то, что дискаунт сервер умеет из 1000 вычитать 200). Поэтому в хорошем тесте должны использоваться настоящие реализации DiscountService и PromoService. А то, что находится за границей бизнес-логики (репозитории, например) - то можно мокать - стабами, моками или фейками - не важно.
И теперь если вдруг кому-то захочется объединить DiscountService и PromoService или вытащить из DiscountService какой-то еще класс, не изменяя поведение - то тесты останутся рабочими (золотое правило: меняется поведение - меняются тесты, поведение неизменное - тесты не меняются).
это просто разные типы тестов. Понятно, что написать какой-то интеграционный тест, который проверит пол бизнес логики проще, чем писать тест на каждый класс. Но какого будет искать потом по всей бизнес логики, а что именно пошло не так? (особенно, если этим занимается человек, который не особо погружен в то, как эта система должна работать).
Не, интеграционный тест - это если у вас еще репозитории работают с БД (реальной или хотя бы in-memory) и входная точка это как минимум API endpoint или обработчик сообщения очереди сообщений. То, что я описал - это как раз юнит-тест в классическом подходе (так называемая Детройтская школа). То, что описываете вы - это юнит-тест Лондонской школы.
Какого будет искать потом по всей бизнес логики, а что именно пошло не так? Достаточно просто - есть изолированное поведение - подсчет скидки по промокоду. Если тест сломался - значит, именно здесь ошибка. Если для анализа изолированного поведения вам нужно перелопатить 20 файлов - значит, у вас архитектурная проблема: скорее всего нужно будет посмотреть две функции по 5 строк кода, но эти две функции будут полностью описывать необходимое поведение, необходимое для понимание. Плюс вы теперь знаете, что ваша система правильно считает скидку по промокоду, а не только правильно вычитает из тысячи двести. Вы можете, глядя на тест, обсудить поведение с аналитиком/продакт-менеджером.
И это все гораздо проще, чем менять десятки моков по всей кодовой базе при простом рефакторинге или разбираться, сломанные тесты - это ошибка в логике или у вас просто еще одна зависимость появилась при рефакторинге.
есть изолированное поведение - подсчет скидки по промокоду
Да, когда у нас демонстрационный или простой и понятный пример - так и происходит. С ростом же бизнес логики, в реальных условиях, это вполне нормальное явление, что количество шагов, чтоб выполнить некоторое действие увеличивается. И тогда перед нами встает выбор: мы либо начинаем атомизировать понятие "модуль" (которое и определяет, что мы называем юнитом, а где уже интеграционный тест), чем в итоге возвращаемся обратно к лондонской школе, либо принимаем, что у нас вот такая бизнес логика и никуда мы от неё не денемся. И нам нужно посмотреть эти 20 файлов (пусть и по 5 строк кода)
Вы можете, глядя на тест, обсудить поведение с аналитиком/продакт-менеджером.
Нууу... Может и можно, а надо ли? Если меня интересует название, входные и выходные данные, то можно что-то ближе к e2e-ам взять (заодно будем уверены, что и апи при этом отрабатывает, и в бд все нормально записывается/достается, и с внешними системами интегрировались, а не только "у нас тут какой-то код в вакууме"). А если прямо обсуждать логику, то это уже в сторону BDD. Для юнита, как-то перебор на мой взгляд.
мы либо начинаем атомизировать понятие "модуль"
И вот тут как раз обычно и начинаются проблемы, потому что границы модуля тестирования определяются на основе деталей реализации. И как правило, в таких случаях тесты становятся хрупкими и с ними больно работать.
Я не очень люблю обсуждать абстрактно, как оно бывает. Ваш пример, хотя бы и минимальный, на самом деле хорошо иллюстрирует реальные сценарии. Поэтому буду отталкиваться от него.
То, что DiscountService использует как внешнюю зависимость PromoService - это деталь реализации. С точки зрения поведения и API класса DiscountService предоставляет метод для вычисления стоимости определенного продукта для определенного пользователя с определенным промокодом. У сервиса есть естественные границы - он откуда-то должен получать данные пользователя и продукта, и скорее всего будет внешняя зависимость для проверки промокодов. С этим ничего не сделаешь. А вот то, что DiscountService нуждается в коллабораторе для вычисления скидки - это уже деталь реализации. Бизнес-правила вообще могут быть такими, что с определенным промокодом все будет стоить 20 или в бесплатно, и тогда отдельный класс для вычисления скидки может оказаться излишним - а может, и не излишним. Поэтому наиболее естественный и простой для понимания тест - это вычисление стоимости продукта у которого есть промокод на 20% с реальной реализацией DiscountService и PromoService. А если мы выделили PromoService в отдельный класс и хотим более гранулярные тесты, чтобы знать из-за чего пофейлилась - то пишем отдельный тест для PromoService.calculateDiscount().
Такой подход хорошо масштабируется для сколь угодно сложных систем.
Согласен, интересный вариант, и в таком виде его можно масштабировать. Но тогда получается, что у нас есть тесты, которые проверяют PromoService.calculateDiscount, promoService.validatePromo итд., что, по идее, определяет их как единицу модуля. Но тогда DiscountServiceTest использует несколько модулей, т.е. строго из определения - это интеграционный тест, который мы почему-то называем юнитом (ну, или считаем, что каждый тест сам определяет, что для него юнит... Но как при этом не сказать, что e2e у нас тоже юнит - не понятно).
Более того, это лишает нас возможности установить четкие правила и границы, когда нужно писать тест, а когда нет (фактически, на усмотрение разработчика, что не надежно и заставляет думать, а не просто действовать по инструкции). Ну, по крайней мере, пока мы не начнем возводить атомарность такого тестирования PromoService.calculateDiscount, promoService.validatePromo и других компонент в абсолют. Но это как раз соответствует тому, что "каждый класс нужно тестировать в изоляции" плюс есть какие-то тесты (пусть мы их тоже будем называть юнитами, не важно), у которых скоуп больше.
Еще один момент в том, что если где-то все же проскакиевает ошибка, у нас падает не 1 тест, который показывает "ошибка вот здесь" (например promoService.validatePromo), а так же падают все тесты (в данном контексте DiscountServiceTest), в которых он как-то используется. Если кто-то еще будет использовать DiscountServiceTest или PromoService.calculateDiscount то он тоже упадет... И те, кто использует их по каскаду. Как в этой толпе сваленных тестов с ходу понять, что смотреть нужно вот сюда - не особо понятно. Можно конечно открыть все 20 файлов, посмотреть, его поля, какие методы вызываются, и найти, но кажется, это не очень удобно.
Молоток — это технический долг
Никак не могу оставить в прошлом, одну историю, произошедшую со мной больше 7 лет назад.
На тот момент я, еще студент последнего курса учаги, только получил свою первую шабашку... Осмотревшись по сторонам, понимаю, что кругом меня не то что других молодых нет, но даже разнорабочих. Сплошные дядьки-петровичи с опытом до колена и красными с похмелья лицами... Ну ничего, сейчас я им покажу, что такое «молодая гвардия» 😂.
Получаю спецовку, стакан и струмент. Мне подробнее рассказывают куда что приколачивать. … И на третьем гвозде головка отлетает и отбивает мне большой палец на левой ноге. Я насаживаю головку обратно, и через три гвоздя она опять отлетает и снова отбивает мне большой палец на левой ноге. Больше того, смущает ситуация, что ни у бригадира, ни у кого из мужиков такого не происходит. Молоток стабилен, да и в нем не меняли ничего уже довольно давно. Вывод: проблема на моей стороне и разбираться мне с ней самому.
…
Пытаюсь откопать, от куда этот молоток мог вообще попасть, и сталкиваюсь с суровой реальностью: количество молотков на складе измеряется штуками и все точно такие же — грязные, с перемотанной синей изолентой, растрескавшейся рукояткой, и без клиньев.
…
Молотки, по своей сути, должны быть простым инструментом для забивания гвоздей. Но вместо этого, он становится источником повышенной опасности. Это не нормально, что вместо того, чтоб забивать гвозди, мы чаще смотрим на код, чтоб понять, как работает тест. Сложность насаживания головки и перематывания рукоятки изолентой на столько большая, что мужики стараются забить болт на забивания гвоздей, заколачивая только «там, где нах надо».
Но ведь мы — работяги: мы рождены, чтобы забивать гвозди без боли в большом пальце левой ноги. Почему мы не можем справиться с какими-то гвоздями? Я утверждаю, что проблема в молотках. То как мы вынуждены их заматывать синим скотчем, и следить за полётом головки провоцирует нас делать строгое разделение: вот тут у нас кувалды, где мы стараемся закрепить головку с помощью клина как следует, а вот тут молотки, к которым стандартные приемы не применимы. Тут мы легко сматываем километры синей изоленты, потому что "ну не покупать же новый молоток, он на две бутылки водки потянет, а то и три".
Я утверждаю, что проблема в моках.
А что именно вы называете моками?
То как мы вынуждены их писать, с ними работать
Кто/что вас вынудил/-о? Каким образом?
провоцирует нас делать строгое разделение: вот тут у нас наши исходники, где мы стараемся писать чисто, красиво и аккуратно, а вот тут тесты, к которым стандартные приемы не применимы.
Что или кто здесь ограничивает применимость "стандартных приёмов"?
Например, кто мешает вместо копипаста портянки под // Arrange вынести этот код в отдельный объект и начать формировать удобную в использовании и недублируемую тестовую инфраструктуру для вашей системы?
Кто мешает разбить этот тест с пятью условиями (из-за которых вы, по всей видимости, тут же ниже жалуетесь на проверку конфигурации) на 5 разных тестов? Которые, кстати, должны были быть написаны в другом месте, в другое время — потому что к тестируемой функциональности не имеют отношения.
Наконец, кто мешает хотя бы проверку цены после применения скидки записать без дублирования magic numbers?
Тут мы легко занимаемся копипастом, потому что "ну не переписывать же эту портянку из
when...thenReturn", тут мы легко делаем бешенную связанность и завязываем наш код на внутреннее состояние, ведь по другому они не конфигруируются и т.д.
То есть, переписывать портянку не хотят разработчики?
Этот отказ от стандартных практик
Отказываются, как следует из контекста, тоже разработчики.
и провоцирует нас еще больше захламлять наши тесты и делать их все меньше пригодными для нас самих.
А "провоцируют" и виноваты в конечном итоге моки?
Получается так, что статью следовал бы назвать "О современной разработке. Часть 1: Плохому танцору и моки — это технический долг".
Мне хорошо помогла книга Хорикова https://www.litres.ru/author/vladimir-horikov/ как работать с моками и стабами, в чем вообще смысл юнит-тестирования и как правильно писать тесты. Могу рекомендовать.
О современной разработке. Часть 1: Моки — это технический долг