Тестирование через реальную границу
Что это такое
Пересечь в тесте настоящую границу — значит позволить двум подлинным компонентам поговорить друг с другом. Запрос уходит в настоящую базу данных, запрос идёт через настоящий маршрутизатор, сообщение публикуется в настоящий брокер, — а тест затем проверяет то, что произвела эта пара. Мастерство в том, чтобы решить, какая граница стоит затрат, и удержать тест детерминированным, когда в дело вступает настоящая инфраструктура.
Почему это важно
У каждой границы, которую пересекает ваш код, есть неявный контракт, и совпадает он с вашим предположением редко. Подделка в памяти с радостью примет значение, которое настоящее хранилище отклонит, вернёт строки в том порядке, в каком вы их записали, а база — нет, или проигнорирует ограничение, которое соблюдает боевая схема. Проверить такие предположения без развёртывания можно только в интеграционных тестах. Однако цена здесь настоящая: тесты медленнее, требуют инфраструктуры и бывают нестабильными — поэтому вопрос проектирования в том, какие стыки этих затрат стоят.
Ключевые идеи
Тестируйте стыки, где контракт ваш
Ценнее всего границы, которые вы контролируете: ваш SQL против вашей схемы, ваша сериализация против вашего парсера, ваш обработчик против маршрутизации фреймворка. На таких стыках любой отказ — дефект в вашем коде. Границу, которая вам не принадлежит — платёжный провайдер, сторонний API, — лучше подменять в тестах, а проверять контролируемо вне них: её поведение не вам исправлять, и её доступность не вам гарантировать.
высшая ценность: ваш запрос <-> ваша схема
ниже ценность: ваш клиент <-> чужой API
Берите настоящую зависимость там, где она может быть настоящей
Для базы данных это обычно тот же движок и та же версия, что в бою, запущенные локально или в контейнере. Другой движок — подмена в памяти или другая версия — возвращает ровно то предположение, которое тест и должен был проверить. Стоит диалекту подмены отличаться, и тест пройдёт, а бой упадёт: набор тестов сделает только хуже, внушив ложную уверенность.
тот же движок, пустая схема, миграции применяет подготовка теста
Пусть каждый тест отвечает за свой мир
Интеграционному тесту нужно известное начальное состояние, и рабочий приём здесь — свежая схема или уникальное пространство имён на каждый прогон. Миграции при этом применяются, а данные создаёт сам тест. Нестабильная слава интеграционных наборов идёт от тестов, которые зависят от строк, оставленных другим тестом: на деле они проверяют порядок, случайно выбранный раннером.
подготовка: создать схему -> применить миграции -> вставить строки, нужные этому случаю
разбор: удалить схему
Держите утверждение рядом со стыком
Проверяйте то, что вернулось через границу, — строки из базы, код состояния и тело от обработчика, — а не случайное состояние где-то в стороне. Чем уже утверждение, тем яснее отказ указывает на нарушенный контракт. Широкое утверждение заставит вас отлаживать ещё до того, как станет понятно, чья сторона виновата.
Управляйте временем, порядком и случайностью
Настоящая инфраструктура обнажает всё недетерминированное в вашем коде: отметки времени, случайные идентификаторы, параллельные вставки, логику, зависящую от часов. Часы передавайте снаружи и фиксируйте там, где от них зависит утверждение, а результаты сортируйте перед сравнением. Нестабильный тест хуже, чем его полное отсутствие. Он приучает команду перезапускать прогон вместо разбирательства.
const now = new Date("2026-03-01T00:00:00Z");
const service = new Signup(repo, { now: () => now });
Практика
Уличите подделку
Возьмите модульный тест, где хранилище данных подменено структурой в памяти. Напишите интеграционный тест на тот же сценарий, но против настоящего хранилища, и посмотрите, что он поймает.
Готово, когда: интеграционный тест либо проходит, либо вы можете назвать конкретное поведение, в котором подделка ошиблась.
Прогоните один и тот же тест дважды
Напишите один интеграционный тест, который создаёт данные для себя и убирает их за собой. Прогоните весь набор дважды подряд, а потом — в обратном порядке.
Готово, когда: каждый прогон зелёный и одинаковый, и между прогонами не переносится никакое состояние.
Частые ошибки
- Подмена на движке, отличном от боевого — тест проходит, а бой падает на том же запросе; исправить: запускайте настоящий движок и версию или признайте, что стык не покрыт.
- Общие строки фикстуры у нескольких тестов — результат зависит от порядка прогона, и набор становится нестабильным; исправить: пусть каждый тест создаёт нужные ему данные.
- Утверждение на случайном состоянии — по отказу не понять, какой контракт нарушен; исправить: проверяйте то, что пересекло границу.
- Инфраструктура, которая живёт от прогона к прогону — данные копятся, и поздние прогоны видят более ранние результаты; исправить: создавайте и удаляйте схему или берите уникальное пространство имён на прогон.
- Настоящие время и порядок — тест проходит во вторник и падает в полночь; исправить: передавайте часы снаружи и сортируйте перед сравнением.
Что читать дальше
- Модульное тестирование (01-cs-fundamentals/10-unit-testing) — изолированный слой, который этот дополняет, и место, с которого стоит начать.
- Фикстуры, базы данных и очистка (01-cs-fundamentals/21-integration-testing/02-fixtures-databases-and-cleanup) — механика подготовки и разбора мира, нужного каждому тесту.
- Сквозное тестирование (01-cs-fundamentals/26-end-to-end-testing) — что делать, когда единственный нужный стык — весь путь пользователя.