Фикстуры, базы данных и очистка

Что это такое

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

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

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

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

Создавайте схему, а не наследуйте её

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

прогон: удалить схему -> миграции -> тесты -> удалить схему

Один тест — один мир

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

тест 1: строки с меткой run=abc-1   тест 2: строки с меткой run=abc-2

Создавайте только то, что нужно случаю

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

const customer = await createCustomer({ tier: "gold" });

Убирайте детерминированно и никогда не полагайтесь на часы

Разбор должен выполняться даже тогда, когда тест упал, иначе пострадает тест, который идёт после. Отметки времени в посеянных данных берите фиксированные, а не «сейчас», и любой проверяемый порядок делайте явным. Тест, который читается как «создано за последние 24 часа», зависит от календаря и однажды упадёт в день, когда за ним никто не следит.

beforeEach: начать транзакцию
afterEach:  откатить (даже при отказе)

Держите секреты и эндпоинты вне теста

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

Практика

Сделайте набор независимым от порядка

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

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

Докажите, что разбор выполняется

Добавьте в один интеграционный тест намеренно падающее утверждение и прогоните набор.

Готово, когда: остальные тесты всё равно проходят, — значит, разбор выполнился несмотря на отказ.

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

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

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

  • Тестирование через реальную границу (01-cs-fundamentals/21-integration-testing/01-testing-across-a-real-boundary) — зачем эти тесты нужны и какие стыки окупают затраты, поэтому читайте первым.
  • Модульное тестирование (01-cs-fundamentals/10-unit-testing) — слой, который покрывает всё, для чего эта механика слишком медленна.
  • Внутреннее устройство баз данных (01-cs-fundamentals/04-database-internals) — миграции, транзакции и поведение схемы, которые эти тесты проверяют по-настоящему.