Тестовые данные в автотестах: источник флака и связности — и как их готовить
Когда автотесты флакают, первым делом грешат на локаторы и тайминги. Но один из самых частых и самых незаметных источников нестабильности — это тестовые данные. Тест, который зависит от записи в общей базе, рано или поздно упадёт: кто-то её поменял, удалил, или два параллельных теста подрались за один и тот же email.
Разберём, как готовить данные так, чтобы тесты были изолированными, детерминированными и переживали параллельный прогон.
Главная боль — общие изменяемые данные
Сценарий-классика: на стенде есть «пользователь [email protected]», и десяток тестов на него завязан. Один тест меняет ему профиль, другой удаляет, третий ждёт его в исходном состоянии. Пока тесты идут последовательно и в одном порядке — везёт. Включаете параллельный прогон или меняете порядок — каскад падений, которые невозможно воспроизвести локально.
Правило: тест не должен зависеть от данных, которые он не создавал сам. Любая общая изменяемая запись — это будущий флак.
Три способа получить данные (и когда что)
- Фикстуры / seed-скрипты: заранее подготовленный набор. Хороши для справочников (страны, тарифы) — неизменяемых данных. Плохи для изменяемых сущностей, которые тест модифицирует.
- Фабрики / билдеры: код создаёт нужную сущность в нужном состоянии прямо в тесте (Object Mother, Test Data Builder). Гибко, читаемо:
aUser().withSubscription(EXPIRED).build(). Лучший дефолт для большинства тестов. - Setup через API: создавать предусловия не через UI, а вызовом API/сервисного слоя — быстро и надёжно. UI используем только для проверяемого сценария, а не для подготовки данных.
Антипаттерн — готовить данные через тот же UI, что тестируешь: медленно, хрупко и смешивает setup с проверкой.
Изоляция: каждый тест сам за себя
- Каждый тест создаёт своё и не рассчитывает на данные «с прошлого раза».
- Fresh fixture (свежее состояние на тест) предпочтительнее shared fixture (общего набора), который тесты молча мутируют.
- Никаких цепочек «тест 1 создал — тест 5 использует»: тесты должны проходить в любом порядке и поодиночке.
Детерминизм: убрать случайность и время
- Время: фиксируйте «сейчас» (clock/freeze), иначе тесты на «старше 18» или «истекло вчера» поплывут в полночь и в високосный год.
- Рандом: Faker (и в Python) — отличный генератор, но с фиксированным seed. «Случайное имя» иногда выдаст пустую строку, апостроф или 60 символов — и тест мигает. Seeded-данные воспроизводимы.
- Порядок: не полагайтесь на порядок выборки из БД без
ORDER BY— он не гарантирован.
Уникальность: пережить параллельный прогон
При параллельных тестах главный враг — коллизии уникальных полей: два теста создают [email protected] → конфликт. Решение — встраивать уникальность в данные: user+{uuid}@test.com, имена с суффиксом-таймстампом/идентификатором воркера. Тогда тесты не дерутся за одни и те же значения.
Очистка: не оставлять мусор
- Teardown: тест удаляет созданное в конце. Минус — при падении до teardown остаётся мусор.
- Транзакционный откат: обернуть тест в транзакцию и откатить. Быстро и чисто, но не работает для многосервисных сценариев и закоммиченных данных.
- Эфемерная БД: поднимать чистую базу на прогон (например, через Testcontainers — см. наш пост про Testcontainers). Максимальная изоляция, ценой ресурсов.
- Уникальные данные (см. выше) частично снимают вопрос уборки: даже не убрав, вы не мешаете следующим прогонам.
Prod-like данные vs синтетика
- Синтетика (фабрики/Faker) — основной источник: контролируемо, безопасно, детерминированно.
- Дамп прода «как есть» в тесты — опасно: это PII/персональные данные (GDPR), а ещё привязывает тесты к случайному состоянию прода.
- Если нужен реализм объёма/распределений — анонимизируйте/маскируйте дамп (имена, email, карты), а не тащите сырой прод.
Антипаттерны (красные флаги)
- Хардкод id/«магических» записей со стенда (
userId = 12345) — сломается при пересоздании стенда. - Цепочки зависимых тестов и общий мутируемый пользователь.
sleep(3)в ожидании, «пока данные появятся/проиндексируются» — ждите по факту готовности (поллинг условия), а не по часам.- Подготовка данных через UI вместо API.
- «Случайные» данные без seed → мигающие тесты, которые винят код.
- Тесты, которые проходят только в определённом порядке.
Чек-лист по тестовым данным (12 пунктов)
- Тест создаёт свои данные и не зависит от «прошлого прогона».
- Fresh fixture по умолчанию; общий мутируемый набор — только для неизменяемых справочников.
- Предусловия готовятся через API/фабрики, а не через тестируемый UI.
- Фабрики/билдеры вместо копипаста объектов (Object Mother / Test Data Builder).
- Время фиксировано (freeze clock) там, где логика зависит от дат.
- Рандом через Faker с фиксированным seed; никаких невоспроизводимых данных.
- Уникальные поля содержат uuid/worker-id, чтобы не коллизить в параллели.
- Очистка определена: teardown / транзакция / эфемерная БД.
- Тесты проходят в любом порядке и поодиночке (рандомизируйте порядок в CI).
- Никаких хардкод-id со стенда.
- Ожидание готовности данных — по условию, не sleep.
- Прод-данные в тестах только анонимизированные; сырой PII — нет.
Итог
Стабильность автотестов держится не только на хороших локаторах и auto-wait, но и на дисциплине с данными. Изоляция (своё на каждый тест), детерминизм (фиксированные время и seed) и уникальность (uuid в полях) убирают целый класс «мигающих» падений, которые иначе списывают на «ну это флак». Готовьте данные кодом, чистите за собой, не тащите сырой прод.