Интеграционные тесты: выявление проблем при взаимодействии компонентов
Вы написали функцию, которая выглядит корректно. Модульный тест проходит. Логика чиста. Затем вы разворачиваете код, и приложение начинает возвращать ошибки. Столбец базы данных, который вы считали существующим, переименовали на прошлой неделе. Внешний API, к которому вы обращаетесь, изменил формат ответа. Сервис, от которого вы зависите, теперь ожидает другой заголовок.
Это пробел, который модульные тесты не могут заполнить. Функция может быть идеально корректной изолированно, но всё равно упасть при попытке взаимодействия с другим компонентом. Интеграционные тесты существуют именно для выявления таких проблем.
Что на самом деле проверяют интеграционные тесты
Интеграционные тесты проверяют, что два или более компонента корректно работают вместе. Компонентами могут быть ваше приложение и база данных, ваш сервис и внешний API или два внутренних сервиса в одной системе.
Ошибки, которые они выявляют, редко связаны с неправильной логикой. Они связаны с несовпадением предположений:
Следующая диаграмма последовательности иллюстрирует типичное несоответствие: сервис отправляет дату как строку, но база данных ожидает timestamp, что вызывает ошибку.
- Ваш код отправляет дату как строку, но столбец базы данных ожидает timestamp.
- Ваш сервис вызывает API с параметром запроса, но API переместил этот параметр в тело запроса.
- Ваше приложение предполагает, что поле всегда присутствует, но вышестоящий сервис включает его только при определённых условиях.
Это не те ошибки, которые можно найти, просто глядя на код. Они проявляются только когда компоненты фактически обмениваются данными.
Ловушка хрупкости
Интеграционные тесты имеют репутацию медленных и хрупких. Эта репутация заслужена. Чем больше реальных компонентов вы задействуете, тем выше вероятность, что тесты упадут по причинам, не связанным с вашим кодом. Тайм-аут сети. Зависимый сервис, который недоступен. Тестовые данные, повреждённые предыдущим запуском.
Когда это происходит repeatedly, команды перестают доверять результатам тестов. Они начинают пропускать интеграционные тесты или игнорировать сбои. Тесты превращаются в шум вместо сигнала.
Решение — не избегать интеграционных тестов. Решение — быть избирательным в том, что вы тестируете с реальными зависимостями.
Выбор того, что тестировать с реальными зависимостями
Не каждая зависимость должна быть реальной в ваших интеграционных тестах. Эмпирическое правило простое: тестируйте с реальной зависимостью только то, что сложно симулировать или что часто вызывает проблемы в продакшене.
Базы данных обычно стоит тестировать с реальным экземпляром. Поведение запросов, ограничения, транзакции и блокировки сложно точно замокать. Мок может сказать вам, что ваш запрос синтаксически корректен, но он не скажет, что ваш запрос вызывает взаимоблокировку при конкурентном доступе или что ваша миграция изменила тип столбца, который ваш код всё ещё обрабатывает как строку.
Внешние сторонние API обычно не стоит тестировать с реальными эндпоинтами в вашем пайплайне. Используйте тестовые дублёры, которые записывают типичные ответы. Риск нестабильных тестов из-за сетевых проблем или ограничений скорости API перевешивает выгоду. Оставьте реальную интеграцию для стейджинга или верификации в продакшене.
Внутренние сервисы в вашей организации находятся посередине. Вы можете тестировать с реальными экземплярами, если интерфейс часто меняется и цена несоответствия высока. В противном случае контрактные тесты часто дают лучший сигнал с меньшей хрупкостью.
Практический способ принять решение: спросите себя: «Если у этой зависимости возникнет проблема, я узнаю об этом из логики приложения или только из того, как она подключается?» Если ответ «из того, как она подключается» — формат ответа, структура заголовков, порядок параметров — то это кандидат на интеграционный тест с реальной зависимостью. Если ответ «из логики приложения», достаточно модульных или контрактных тестов.
Как сделать интеграционные тесты быстрыми и надёжными
После того как вы решили, что тестировать с реальными зависимостями, следуйте этим практикам, чтобы интеграционные тесты оставались полезными:
Тестируйте только соединение, а не бизнес-логику. Если у вас уже есть модульные тесты, покрывающие бизнес-правила, не повторяйте их в интеграционных тестах. Интеграционный тест для запроса к базе данных должен проверить, что запрос успешно выполняется против реальной базы данных и возвращает ожидаемую структуру. Он не должен проверять каждый граничный случай бизнес-логики, использующей этот запрос.
Сбрасывайте окружение перед каждым тестом. Если вы используете базу данных, создавайте изолированные тестовые данные и очищайте их после завершения теста. Тесты, зависящие от состояния, оставленного предыдущими тестами, хрупки и сложны в отладке. Используйте транзакции базы данных, которые откатываются после каждого теста, или поднимайте свежий тестовый контейнер для каждого запуска тестов.
Ограничьте количество интеграционных тестов. Вам не нужно тестировать каждую комбинацию параметров. Протестируйте один счастливый путь и несколько реалистичных сценариев сбоя. Цель — уверенность, что соединение работает, а не покрытие каждого возможного входа.
Где интеграционные тесты размещаются в вашем пайплайне
Интеграционные тесты находятся между модульными и сквозными тестами. Они дороже модульных, но быстрее и более сфокусированы, чем сквозные.
Типичный пайплайн сначала запускает модульные тесты. Если они проходят, пайплайн запускает интеграционные тесты. Если интеграционные тесты проходят, пайплайн переходит к развёртыванию на стейджинг или в продакшен. Сквозные тесты, если они есть, запускаются позже или в отдельном окружении.
Цель интеграционных тестов — не достижение процента покрытия. Цель — дать вам уверенность, что при изменении кода соединения между компонентами всё ещё работают.
Практический чек-лист
- Для каждой внешней зависимости решите: тестировать с реальным экземпляром, тестировать с тестовым дублёром или полагаться на контрактные тесты.
- Запускайте интеграционные тесты в изолированном окружении, которое можно сбросить до известного состояния.
- Сосредоточьте интеграционные тесты на поведении соединения, а не на бизнес-логике.
- Ограничьте интеграционные тесты одним счастливым путём и несколькими реалистичными сценариями сбоя.
- Отслеживайте время выполнения тестов. Если интеграционные тесты выполняются дольше, чем модульные, вероятно, их слишком много или они неправильного типа.
Вывод
Интеграционные тесты отвечают на вопрос, на который модульные тесты ответить не могут: «Действительно ли эти компоненты работают вместе?» Ответ стоит получить до развёртывания. Но интеграционные тесты — это инструмент, а не цель. Будьте избирательны в том, что тестируете с реальными зависимостями, поддерживайте тесты быстрыми и изолированными и используйте их для укрепления уверенности в развёртываниях, а не для погони за цифрами покрытия.