Прямой ответ
Flaky-тест — тест, который при неизменном коде иногда проходит, а иногда падает. Главная опасность не в самом падении, а в реакции команды: после нескольких ложных срабатываний разработчики перестают верить тестам и перезапускают прогон вместо разбора.
Основные причины, по частоте встречаемости:
- Зависимость от времени.
datetime.now()в assertions, таймауты, вычисления «сегодня», сравнение с датой создания записи. - Общее состояние между тестами. Записи в БД, файлы, переменные окружения, кэши модулей, глобальные объекты, оставшиеся от предыдущего теста.
- Порядок выполнения. Тест работает, только если запущен после другого. Проявляется при перестановке, параллелизации или выборе подмножества.
- Гонки в асинхронном коде. Ожидание события через
sleepвместо явного условия; два потока или корутины обращаются к общему ресурсу. - Внешние зависимости. Сеть, сторонние API, реальные сервисы. Даже локальный Docker-контейнер может не успеть подняться.
- Случайные данные. Значения из генератора попадают в поля с ограничениями или в проверяемую логику.
- Недетерминированный порядок итерации. Множества, словари без сортировки, параллельные структуры.
- Числа с плавающей точкой. Сравнение через
==вместо допуска. - Ресурсы машины. Недостаток памяти или CPU под нагрузкой меняет тайминги и исчерпывает лимиты.
Диагностика начинается с воспроизведения. Полезные приёмы:
# повтор одного теста много раз
pytest tests/test_orders.py::test_refund --count=50 -x # pytest-repeat
# запуск в обратном порядке — ловит зависимость от порядка
pytest --reverse # pytest-reverse
# параллельный запуск — ловит общее состояние и гонки
pytest -n 8 --dist loadscope
Практика: устранение по типам причин
1. Заморозьте время. Все обращения к текущему времени выносят за скоуп одной функции, которую тест подменяет. Это убирает самую массовую категорию падений.
from freezegun import freeze_time
@freeze_time("2026-03-01 12:00:00")
def test_invoice_due_in_30_days(invoice_factory):
inv = invoice_factory(created_at=datetime(2026, 2, 1))
assert days_until_due(inv) == 30
2. Изолируйте состояние. Каждый тест работает в транзакции с откатом, во временном каталоге или с независимым контейнером. О подходах к изоляции — в статье про интеграционные тесты в Docker.
3. Уберите скрытый порядок. Тест не должен полагаться на то, что другой тест уже что-то создал. Если общий контекст действительно нужен, его создаёт фикстура, а не соседний тест.
4. Замените sleep на ожидание условия.
# плохо: фиксированная пауза
time.sleep(2)
assert job.status == "done"
# хорошо: ожидание условия с лимитом
deadline = time.monotonic() + 10
while job.status != "done" and time.monotonic() < deadline:
time.sleep(0.05)
assert job.status == "done"
5. Сделайте данные детерминированными. Уникальность — через счётчики, а не случайные значения; проверяемые значения — явно. Подробности — в статье про фабрики тестовых данных.
6. Подменяйте внешнее. Сеть в unit-тестах заменяют двойниками; реальный сервис остаётся в отдельных интеграционных прогонах — см. тестовые двойники.
7. Сортируйте то, что сравниваете. Сравнение списков, полученных из множества или параллельной обработки, требует явной сортировки.
Рекомендации по процессу:
- Ведите учёт. Каждый flaky-инцидент фиксируют: тест, частота, предполагаемая причина, статус. Без учёта проблема не исчезает, потому что не видна.
- Карантин — временно. Переместить тест в отдельный набор, который не блокирует слияние, допустимо. Но у карантина должен быть срок и ответственный; бессрочный карантин равен удалению теста.
- Не перезапускайте прогон автоматически без учёта. Автоперезапуск скрывает проблему: команда видит зелёный результат, а тест продолжает падать.
- Проверяйте на параллельном прогоне. Если тест не проходит при параллельном запуске, он не изолирован — это дефект теста, даже если последовательный прогон стабилен.
- Устраняйте причину, а не симптом. Увеличение таймаута помогает только если причина в медленной машине; при гонке оно лишь меняет вероятность.
- Проверяйте исправление повторным прогоном. После правки тест запускают десятки раз подряд, а не один раз: единичный успешный прогон ничего не доказывает, если исходная частота падений была несколько процентов.
- Учитывайте нагрузку на машину прогона. Один и тот же набор может быть стабилен на свободной машине разработчика и нестабилен в CI, где параллельно идут другие задания; проверка в условиях CI обязательна.
Практический сценарий: 4 % падений в CI и неделя разбора
Контекст: пайплайн падал примерно в 4 % прогонов, всегда в разных тестах. Команда привыкла перезапускать, реальная причина оставалась неизвестной.
- Измерение. За две недели собрали статистику: 23 падения, из них 14 в трёх тестах работы с заказами, 6 в тестах отчётов, 3 — разные.
- Причина 1 (14 падений). Тесты использовали реальное текущее время для расчёта просрочки. В ночь перехода на летнее время и в конце месяца ожидаемое число дней менялось.
- Причина 2 (6 падений). Тесты отчётов читали записи, созданные другим тестом, который при параллельном запуске выполнялся позже. Порядок не гарантировался.
- Причина 3 (3 падения). Случайная сумма из генератора иногда округлялась до значения, при котором срабатывала другая ветка расчёта.
- Действия. (а) Ввели заморозку времени во всех тестах с датами; (б) данные для отчётов создали локальной фикстурой вместо зависимости от соседнего теста; (в) в фабрике сумму сделали детерминированной с явным переопределением в тестах; (г) добавили прогон с параллелизацией и в обратном порядке в ночной CI; (д) завели список flaky-инцидентов с ответственными.
- Результат. За следующие четыре недели падений не было; перезапуски пайплайна перестали быть обычной практикой.
Вывод: нестабильность почти всегда имеет конкретную и устранимую причину. Основной барьер — не сложность исправления, а отсутствие учёта: без статистики команда борется со случайными проявлениями, а не с источником.
FAQ
Можно ли просто помечать flaky-тесты как ожидаемо нестабильные? Можно как временную меру с явным сроком. Постоянная пометка превращает тест в декорацию: он перестаёт защищать от регрессий, но продолжает создавать видимость проверки.
Правда ли, что чаще всего виновато время? По практике — да, вместе с общим состоянием. Эти две категории покрывают большинство случаев; гонки и сеть дают меньшую долю, но требуют более сложного исправления.
Стоит ли использовать автоматические повторные запуски отдельных тестов? Для редких и хорошо изученных случаев, связанных с внешней инфраструктурой, — допустимо, при условии учёта каждого срабатывания. Для тестов бизнес-логики повтор маскирует дефект.
Как понять, что тест изолирован? Он проходит при запуске в одиночку, в обратном порядке, в параллельном прогоне и на другой машине. Если хотя бы один из этих прогонов нестабилен, изоляции нет.
Источники: pytest docs, Google Research (Flaky Tests at Scale), Martin Fowler (non-determinism) — ссылки в шапке.