Почему модульные тесты должны быть первым этапом вашего пайплайна
Представьте: вы пушите изменение кода в пятницу после обеда. Сборка проходит, деплой выполняется, и вы уходите домой. В субботу утром телефон разрывается от оповещений. Расчёт скидки применяет отрицательные значения к заказам клиентов. Логика выглядела корректной на ревью. Но никто не заметил граничный случай, когда купон в сочетании с акционной ценой даёт отрицательный итог.
Именно для таких проблем и существуют модульные тесты. Не потому что они сложные, а потому что они выполняются быстро, рано и изолированно. Это первая линия обороны в любом пайплайне доставки.
Что на самом деле проверяют модульные тесты
Модульный тест проверяет одно осмысленное поведение, начиная с точки входа, где это поведение инициируется. В бэкенд-сервисе такой точкой входа может быть REST-эндпоинт или вариант использования. Запросу разрешается проходить через реальные внутренние слои: контроллер, сервис, доменную логику и границу репозитория. Тест не пытается доказать, что один метод вызывает другой. Он пытается доказать, что система корректно реагирует на осмысленный вход.
Если у вас есть функция, рассчитывающая стоимость доставки на основе веса и пункта назначения, модульный тест может подтвердить:
- Стандартный вес даёт стандартную стоимость
- Нулевой вес даёт нулевую стоимость
- Отрицательный вес возвращает ошибку или ноль
- Максимальный вес возвращает предельное значение
Чего модульный тест не должен пытаться доказать — так это то, что реальная база данных корректно сохраняет эту стоимость, что реальный платёжный шлюз её принимает или что соседний сервис действительно доступен. Это задачи для других типов тестов.
Ценность модульных тестов узка, но глубока. Они дают уверенность, что конкретное поведение всё ещё работает, когда окружение контролируется. Когда вы позже меняете код, проходящие модульные тесты говорят вам, что вы не сломали уже проверенное поведение.
Принцип изоляции
Чтобы модульные тесты были быстрыми и надёжными, внешнее окружение должно быть контролируемым. Внутренние слои приложения должны работать как обычно. Внешние соседи не должны влиять на результат теста. Обычно это означает отсутствие реального подключения к production-базе данных, живого HTTP-вызова к стороннему сервису или зависимости от другого запущенного сервиса. Если поведению нужны данные, тест может использовать контрольные данные, локальную тестовую базу данных в памяти или фейк, мок или стаб на границе системы.
Именно поэтому модульное тестирование не следует определять как «один тест на метод» или «один тест на класс». Это слишком механистичное определение, которое часто подталкивает команды к тестированию деталей реализации. Модульный тест лучше понимать как тест поведения от соответствующей точки входа, при этом внешний мир контролируется настолько, что сбой указывает именно на тестируемое поведение.
Мобильный код — полезный пример. Некоторое поведение имеет смысл только внутри мобильной среды выполнения. В этом случае использование эмулятора или симулятора не делает тест автоматически неправильным. Вопрос в том, фокусируется ли тест на одном поведении и контролирует ли зависимости вокруг него. Если да, он всё ещё может выполнять роль модульного теста в пайплайне.
Именно эта изоляция делает модульные тесты быстрыми. Хорошо написанный набор модульных тестов для типичного бэкенд-сервиса выполняется за секунды, а не минуты. Сравните это с интеграционными тестами, которые поднимают контейнеры или подключаются к тестовым базам данных. Они занимают минуты.
Скорость имеет значение, потому что быстрые тесты запускаются чаще. Разработчики запускают их локально перед пушем кода. CI-пайплайны запускают их сразу после сборки. Если что-то ломается, вы узнаёте об этом через секунды или минуты, а не после ожидания полного набора тестов, который занимает полчаса.
Где модульные тесты размещаются в пайплайне
Модульные тесты должны быть на самом раннем этапе вашего пайплайна, сразу после компиляции или сборки кода. Логика проста: если базовое поведение, предоставляемое приложением, уже сломано, нет смысла запускать более медленные тесты, зависящие от реальных соседних систем.
Типичный порядок этапов пайплайна выглядит так:
Вот практический пример того, как этот первый шаг выглядит в конфигурационном файле CI:
# .gitlab-ci.yml или аналогичный конфиг CI
stages:
- build
- test
- deploy
build:
stage: build
script:
- npm install
- npm run build
test-unit:
stage: test
script:
- npm test -- --coverage
only:
- merge_requests
- main
- Сборка или компиляция кода
- Запуск модульных тестов
- Запуск статического анализа или линтинга
- Сборка образов контейнеров или артефактов
- Запуск интеграционных тестов
- Деплой на стейджинг
- Запуск сквозных или приёмочных тестов
- Деплой в production
Если модульные тесты падают на шаге 2, пайплайн останавливается. Никакие контейнеры не собираются. Никакое стейджинговое окружение не занимается. Никакое время не тратится впустую на ожидание интеграционных тестов, которые всё равно упадут, потому что базовая логика неверна.
Это и есть быстрый цикл обратной связи. Чем раньше вы ловите баг, тем дешевле его исправить. Баг, найденный во время модульного тестирования, исправляется за минуты. Баг, найденный в production, требует реагирования на инцидент, процедур отката, коммуникации с клиентами и, возможно, восстановления данных.
Когда модульных тестов недостаточно
У модульных тестов есть слепая зона: они не могут проверить, как компоненты работают вместе в реальной системе. Если ваше поведение оформления заказа зависит от внешнего платёжного API, модульный тест может проверить, как ваш код ведёт себя, когда платёжная зависимость возвращает успех, ошибку, таймаут или искажённые данные. Он не может сказать вам, принимает ли реальный платёжный API ваш формат запроса, обрабатывает ли аутентификацию или возвращает ожидаемую структуру ответа.
Для этого нужны интеграционные тесты. Но модульные тесты всё равно служат здесь цели. Они проверяют, что структура вашего кода корректна, параметры передаются в правильном порядке и обработка ошибок работает как ожидается. Они просто не могут заменить реальную интеграционную проверку.
Ещё одно ограничение — модульные тесты не могут выявить проблемы конфигурации, различия в окружении или инфраструктурные проблемы. Функция, которая отлично работает в модульных тестах, может упасть в production, потому что в production-базе данных другая настройка сортировки или отсутствует необходимая переменная окружения. Эти проблемы требуют других подходов к тестированию.
Сколько модульного тестирования достаточно
Ответ зависит от риска. Если функция реализует основную бизнес-логику, где ошибка может привести к финансовым потерям, повреждению данных или проблемам безопасности, вам нужны тщательные модульные тесты, покрывающие нормальные случаи, граничные случаи, ошибочные случаи и пограничные условия.
Если функция просто передаёт данные из одного места в другое без преобразования, одного модульного теста, подтверждающего корректность сквозной передачи, может быть достаточно. Тратить часы на написание исчерпывающих тестов для тривиального кода — не лучшее использование времени.
Практический подход — тестирование на основе рисков. Определите, какие части вашей кодовой базы несут наибольший риск в случае сбоя. Сосредоточьте усилия по модульному тестированию там. Для низкорискового кода пишите ровно столько тестов, чтобы поймать очевидные ошибки.
Практический чек-лист для модульных тестов в вашем пайплайне
- Модульные тесты запускаются до любых интеграционных или сквозных тестов
- Модульные тесты выполняются за несколько минут для всей кодовой базы
- Модульные тесты не требуют внешних сервисов, баз данных или сетевого доступа
- Каждый модульный тест проверяет одно осмысленное поведение от соответствующей точки входа, а не один метод или класс
- Тесты детерминированы: одинаковый вход всегда даёт одинаковый результат
- Упавшие модульные тесты немедленно останавливают пайплайн
- Разработчики могут запускать те же тесты локально перед пушем
Конкретный вывод
Модульные тесты — это не про достижение идеальных показателей покрытия. Они про выявление наиболее распространённого класса багов как можно раньше, с минимальными затратами времени и инфраструктуры. Разместите их первыми в вашем пайплайне, держите их быстрыми, держите их изолированными и сосредоточьте на логике, которая важнее всего. Всё остальное строится на этом фундаменте.