Есть несколько практик в разработке ПО, которые, с одной стороны, являются практически неотъемлемой частью пейзажа, а с другой - довольно уродливы, если вдуматься, и сильно вредят всем (некоторые вредят AI Code-ассистентам). В этом посте я хочу поныть про автоматические тесты - священную корову разработки ПО последних как минимум 10 лет. И особенно поныть про unit-тесты.

В чем идея автотестов в целом - у нас есть код, который мы можем запустить, и он с предсказуемым результатом проверит, что наше приложение после изменений кода все еще рабочее.

В целом идея отличная, и я на 100% за нее. Без автотестов мы скатываемся в ситуацию, когда полное ручное тестирование более-менее большого приложения занимает больше спринта, и разрабатывать код становится либо очень дорого, либо почти невозможно. Ну и человеческий фактор тоже дает о себе знать: QA-инженеры не роботы и могут ошибаться.

В теории все отлично. Но есть несколько сложных моментов, которые в 80% случаев игнорируются и ведут к крайне уродливым последствиям.

Стабильность контракта

Обвязывать автотестами надо внешний контракт приложения/системы, а не реализацию, т. к. именно он меняется не так часто и имеет ценность для конечного пользователя. Как правило, это довольно затратно, и часто системы вообще не имеют достаточно хорошо описанного и, главное, стабильного внешнего контракта. Потому что про это надо думать и проектировать, а делать это некому - у нас agile и команда, у которой лапки.

Это неизбежно ведет к появлению "мертвых тестов". Тест есть, но он тестирует реализацию, которая устарела, и тест сломался. Но нам надо релизить - давайте его вырубим.

Должно быть железное правило: "сломанный тест == сломанный функционал у пользователя". Если тест сломан, но все работает и можно релизить - надо удалить тест и, возможно, провести воспитательную беседу с тем, кто его создавал.

Edge cases

Хороший тест написать примерно в два раза ментально сложнее, чем написать сам функционал. Надо учесть и создать все возможные комбинации нетривиальных условий. Надо думать: "как сломать то, что я написал". Тут категорически не подходят тесты, написанные LLM - она вообще не умеет ломать. Да и обычному разработчику написать хороший тест очень нелегко.

Это ведет к тому, что менеджмент почти никогда не рассуждает так: "вот у нас есть рабочий код, написанный мидлом, теперь пусть наш супер-бизон Петя напишет к нему нормальный тест". Обычно все всегда наоборот, и в результате тест получается "на отвяжись". Это дает команде ложную уверенность, что тест как бы есть.

Окружение

В более-менее реальной системе есть 100500 зависимостей, большие базы данных и масса нетривиальных бизнес-правил.

Написать тест, который эмулирует прохождение документа от живого кладовщика Рустама к живому бухгалтеру Алефтине через 5 сервисов, две Kafka с логированием в аудит-логи и отправкой промежуточных данных в ФНС через их API - так, чтобы опечатки, сделанные Рустамом, корректно везде обработались и Рустам, Алефтина и ФНС были OK с результатом, - задача более чем нетривиальная.

Но именно это и есть тест, который проверяет, что система работает end-to-end.

Бывает так, что такой тест вообще невозможен, и тогда его либо делают на моках (отвратительно), либо оставляют на откуп живым QA. И то и другое нормально, но тут важно, чтобы все участники процесса понимали, что они делают и какие у процесса ограничения.

Или, если такой тест все-таки создают, он может работать, скажем, 5 часов (самый длинный тест, который я видел, работал 23 часа). Это должно вести к исключению теста из процесса PR pre-merge check, что на практике делается не всегда, и разработчики пьют кофе и играют в бильярд, пока крутится тест, вместо того чтобы писать код.

Unit-тесты

Проклятие IT-индустрии. Ну, то есть в идеале - штука-то хорошая: "давайте мы будем тестировать мельчайший контракт отдельным атомарным быстрым тестом". Проблема в том, что это применимо, как правило, только к алгоритмам. Например, функция, которая ищет кратчайший путь на карте. Сделать для нее тест - благое и в целом достаточно простое дело.

Теперь давайте предположим, что мы делаем систему, которая создает групповые бронирования отелей, опираясь на два разных API. Каков шанс, что во всей системе есть хоть что-то, что можно тестировать unit-тестом? Я бы сказал - 0,1%. Ну, там может быть какой-то алгоритм вычисления тарифов или что-то такое. Но, скорее всего, он будет сильно зависеть от типов данных API, и без API его тестировать бессмысленно.

Что происходит на практике (и тут я почти слово в слово цитирую живых менеджеров из нескольких не связанных между собой проектов):

  • Unit-тесты почти бесполезны (объясняю написанное выше, показываю пример в коде, когда либо тест сломан, а система работает, либо наоборот).

  • Да, но делать end-to-end-тесты дорого и, главное, сложно. Пусть разработчики тратят до 30% времени на unit-тесты (последний писк маразма - пусть тесты пишет Copilot). У нас будет высокое покрытие кода, это красиво смотрится в отчетах и отвечает на вопрос "как вы обеспечиваете качество".

Т. е. по факту unit-тесты, за редким исключением, служат ритуальной цели - показать процесс обеспечения качества.

Тест == расходы

Код и тесты - это чистой воды накладные расходы, тест не приносит никакой непосредственной прибыли. Он стоит денег не только и не столько в момент написания, но и создает постоянную статью расхода на прогон и поддержание в актуальном состоянии все свое время жизни.

Это не значит, что автотесты не надо писать, это значит, что решение, что и как именно тестировать и сколько это будет стоить сейчас и потом, должно осознанно приниматься в контексте общего бюджета проекта.

Фраза, которую часто используют команды, чтобы похвастаться - "мы написали 10+ тестов к этому методу" - на самом деле значит "мы сожгли вот столько денег и заложили постоянный расход в $N на все будущие спринты". Вопрос, который хочется тут задать: "вы точно все просчитали, и оно того стоит, и у вас есть столько денег в бюджете?"

Тесты либо проходят, либо нет

Одна из очень "популярных" ошибок - тесты Шредингера. Они иногда проходят, иногда падают: давайте их перезапустим, ой, все равно красный, но давайте все равно релизиться.

Есть несколько глупых причин, по которым члены команды считают нормальным так поступать. Но, на мой взгляд, ни одна из них не является разумной. Тест - это довольно четкая математическая штука: система либо проходит тесты, либо нет. Если нет - надо править либо код, либо тесты. Если тест можно игнорировать, то его надо удалить. Процесс разработки ПО и так не простой, и вносить туда еще элемент квантовой неопределенности - плохая идея.

Пример

Вот у меня есть БД с какими-то транзакционными данными, есть DTO, обернутые вокруг таблиц, есть репозиторий, есть сервис и есть контроллер. Достаточно популярная техническая архитектура.

Что можно протестировать? Контроллер имеет Swagger-описание. Можно запустить при помощи Testcontainers настоящую БД, создать там схему с тестовыми данными, поднять сервис и через HTTP "дергать" методы, проверяя, что они работают так, как было задумано.

Это стабильный тест, опирающийся только на контракт. Он будет меняться только вместе с контрактом (ну или когда найдется не протестированный edge case). Если тест сломан - то на 100% сломан сервис, и "красный" результат никак нельзя игнорировать.

Подводя итог

Как я вижу правильный процесс организации работы с автотестами в команде:

  1. Тестировать только то, что имеет четкий, продуманный, стабильный контракт с понятным циклом изменений. Например, Swagger-файл для REST API, причем этот Swagger-файл имеет продуманный порядок внесения изменений.

  2. Тесты пишутся теми, кто профессионально способен написать нормальный, полезный тест, а не джунами по остаточному принципу.

  3. Анализируется стоимость тестов, и тесты группируются в зависимости от стоимости. Тесты, которые можно прогнать за 1-2 минуты, живут в быстрой группе для разработчиков; те, что требуют до 15 минут, могут прогоняться перед мерджем; те, что требуют больше 15 минут, прогоняются при выпуске релиза (либо ночью, либо асинхронно, так, чтобы не тормозить команду).

  4. Все участники процесса, особенно менеджмент, четко понимают, что покрывается автотестами, а что нет, и что с этим делать с точки зрения обеспечения качества софта.

  5. Unit-тесты пишутся только на классы/функции, имеющие четкий алгоритмический контракт. В общем случае (наверное, есть исключения, но они мне сейчас в голову не приходят) ничего не мокаeтся. Если вы мокаeте поведение базы данных, то, скорее всего, вы ничего не тестируете.

Комментарии (1)


  1. YegorP
    17.01.2026 15:55

    Unit-тесты
    Проклятие IT-индустрии. Ну, то есть в идеале - штука-то хорошая: "давайте мы будем тестировать мельчайший контракт отдельным атомарным быстрым тестом". Проблема в том, что это применимо, как правило, только к алгоритмам. Например, функция, которая ищет кратчайший путь на карте. Сделать для нее тест - благое и в целом достаточно простое дело.

    Юнит-тесты вполне можно пускать в настоящую базу, и это может очень сильно поднять их полезность. База поднимается in-memory, каждому воркеру своя. Отдельные тесты или сюиты за собой подчищают.

    Кто сомневается в скорости: 2400 тестов, 90% которых лезут в базу, прогоняются за 2-3 минуты. Около 50к LOC кода и такой же объём тестов. Отдельно взятый тест прогоняется секунд за 5-10, то есть для активного кодинга и дебпггинга подходит.

    И нет, это не интеграционные тесты. Они тестируют поведение отдельных модулей. Я в курсе про религию, которая строго заставляет СУБД мокать и абстрагировать. Но мы такого не исповедуем и рассматриваем её как данность рантайма. Никто же не мокает арифметику, например. Вот и мы жёстко завязались на конкретную СУБД и не стесняемся этого.

    Короче кошек надо уметь готовить как всегда.