Тестирование через реальную границу

Что это такое

Пересечь в тесте настоящую границу — значит позволить двум подлинным компонентам поговорить друг с другом. Запрос уходит в настоящую базу данных, запрос идёт через настоящий маршрутизатор, сообщение публикуется в настоящий брокер, — а тест затем проверяет то, что произвела эта пара. Мастерство в том, чтобы решить, какая граница стоит затрат, и удержать тест детерминированным, когда в дело вступает настоящая инфраструктура.

Почему это важно

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

Ключевые идеи

Тестируйте стыки, где контракт ваш

Ценнее всего границы, которые вы контролируете: ваш SQL против вашей схемы, ваша сериализация против вашего парсера, ваш обработчик против маршрутизации фреймворка. На таких стыках любой отказ — дефект в вашем коде. Границу, которая вам не принадлежит — платёжный провайдер, сторонний API, — лучше подменять в тестах, а проверять контролируемо вне них: её поведение не вам исправлять, и её доступность не вам гарантировать.

высшая ценность: ваш запрос <-> ваша схема
ниже ценность:   ваш клиент <-> чужой API

Берите настоящую зависимость там, где она может быть настоящей

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

тот же движок, пустая схема, миграции применяет подготовка теста

Пусть каждый тест отвечает за свой мир

Интеграционному тесту нужно известное начальное состояние, и рабочий приём здесь — свежая схема или уникальное пространство имён на каждый прогон. Миграции при этом применяются, а данные создаёт сам тест. Нестабильная слава интеграционных наборов идёт от тестов, которые зависят от строк, оставленных другим тестом: на деле они проверяют порядок, случайно выбранный раннером.

подготовка: создать схему -> применить миграции -> вставить строки, нужные этому случаю
разбор:     удалить схему

Держите утверждение рядом со стыком

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

Управляйте временем, порядком и случайностью

Настоящая инфраструктура обнажает всё недетерминированное в вашем коде: отметки времени, случайные идентификаторы, параллельные вставки, логику, зависящую от часов. Часы передавайте снаружи и фиксируйте там, где от них зависит утверждение, а результаты сортируйте перед сравнением. Нестабильный тест хуже, чем его полное отсутствие. Он приучает команду перезапускать прогон вместо разбирательства.

const now = new Date("2026-03-01T00:00:00Z");
const service = new Signup(repo, { now: () => now });

Практика

Уличите подделку

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

Готово, когда: интеграционный тест либо проходит, либо вы можете назвать конкретное поведение, в котором подделка ошиблась.

Прогоните один и тот же тест дважды

Напишите один интеграционный тест, который создаёт данные для себя и убирает их за собой. Прогоните весь набор дважды подряд, а потом — в обратном порядке.

Готово, когда: каждый прогон зелёный и одинаковый, и между прогонами не переносится никакое состояние.

Частые ошибки

  • Подмена на движке, отличном от боевого — тест проходит, а бой падает на том же запросе; исправить: запускайте настоящий движок и версию или признайте, что стык не покрыт.
  • Общие строки фикстуры у нескольких тестов — результат зависит от порядка прогона, и набор становится нестабильным; исправить: пусть каждый тест создаёт нужные ему данные.
  • Утверждение на случайном состоянии — по отказу не понять, какой контракт нарушен; исправить: проверяйте то, что пересекло границу.
  • Инфраструктура, которая живёт от прогона к прогону — данные копятся, и поздние прогоны видят более ранние результаты; исправить: создавайте и удаляйте схему или берите уникальное пространство имён на прогон.
  • Настоящие время и порядок — тест проходит во вторник и падает в полночь; исправить: передавайте часы снаружи и сортируйте перед сравнением.

Что читать дальше