Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Flaky-тесты: причины нестабильности и устранение

Flaky-тесты: почему тесты падают через раз и как это исправить

Flaky-тесты: время, порядок, состояние, гонки и сеть как причины нестабильности, диагностика, карантин и процесс устранения.

Прямой ответ

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

Основные причины, по частоте встречаемости:

  1. Зависимость от времени. datetime.now() в assertions, таймауты, вычисления «сегодня», сравнение с датой создания записи.
  2. Общее состояние между тестами. Записи в БД, файлы, переменные окружения, кэши модулей, глобальные объекты, оставшиеся от предыдущего теста.
  3. Порядок выполнения. Тест работает, только если запущен после другого. Проявляется при перестановке, параллелизации или выборе подмножества.
  4. Гонки в асинхронном коде. Ожидание события через sleep вместо явного условия; два потока или корутины обращаются к общему ресурсу.
  5. Внешние зависимости. Сеть, сторонние API, реальные сервисы. Даже локальный Docker-контейнер может не успеть подняться.
  6. Случайные данные. Значения из генератора попадают в поля с ограничениями или в проверяемую логику.
  7. Недетерминированный порядок итерации. Множества, словари без сортировки, параллельные структуры.
  8. Числа с плавающей точкой. Сравнение через == вместо допуска.
  9. Ресурсы машины. Недостаток памяти или 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. Сортируйте то, что сравниваете. Сравнение списков, полученных из множества или параллельной обработки, требует явной сортировки.

Рекомендации по процессу:

  1. Ведите учёт. Каждый flaky-инцидент фиксируют: тест, частота, предполагаемая причина, статус. Без учёта проблема не исчезает, потому что не видна.
  2. Карантин — временно. Переместить тест в отдельный набор, который не блокирует слияние, допустимо. Но у карантина должен быть срок и ответственный; бессрочный карантин равен удалению теста.
  3. Не перезапускайте прогон автоматически без учёта. Автоперезапуск скрывает проблему: команда видит зелёный результат, а тест продолжает падать.
  4. Проверяйте на параллельном прогоне. Если тест не проходит при параллельном запуске, он не изолирован — это дефект теста, даже если последовательный прогон стабилен.
  5. Устраняйте причину, а не симптом. Увеличение таймаута помогает только если причина в медленной машине; при гонке оно лишь меняет вероятность.
  6. Проверяйте исправление повторным прогоном. После правки тест запускают десятки раз подряд, а не один раз: единичный успешный прогон ничего не доказывает, если исходная частота падений была несколько процентов.
  7. Учитывайте нагрузку на машину прогона. Один и тот же набор может быть стабилен на свободной машине разработчика и нестабилен в CI, где параллельно идут другие задания; проверка в условиях CI обязательна.

Практический сценарий: 4 % падений в CI и неделя разбора

Контекст: пайплайн падал примерно в 4 % прогонов, всегда в разных тестах. Команда привыкла перезапускать, реальная причина оставалась неизвестной.

  1. Измерение. За две недели собрали статистику: 23 падения, из них 14 в трёх тестах работы с заказами, 6 в тестах отчётов, 3 — разные.
  2. Причина 1 (14 падений). Тесты использовали реальное текущее время для расчёта просрочки. В ночь перехода на летнее время и в конце месяца ожидаемое число дней менялось.
  3. Причина 2 (6 падений). Тесты отчётов читали записи, созданные другим тестом, который при параллельном запуске выполнялся позже. Порядок не гарантировался.
  4. Причина 3 (3 падения). Случайная сумма из генератора иногда округлялась до значения, при котором срабатывала другая ветка расчёта.
  5. Действия. (а) Ввели заморозку времени во всех тестах с датами; (б) данные для отчётов создали локальной фикстурой вместо зависимости от соседнего теста; (в) в фабрике сумму сделали детерминированной с явным переопределением в тестах; (г) добавили прогон с параллелизацией и в обратном порядке в ночной CI; (д) завели список flaky-инцидентов с ответственными.
  6. Результат. За следующие четыре недели падений не было; перезапуски пайплайна перестали быть обычной практикой.

Вывод: нестабильность почти всегда имеет конкретную и устранимую причину. Основной барьер — не сложность исправления, а отсутствие учёта: без статистики команда борется со случайными проявлениями, а не с источником.

FAQ

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

Правда ли, что чаще всего виновато время? По практике — да, вместе с общим состоянием. Эти две категории покрывают большинство случаев; гонки и сеть дают меньшую долю, но требуют более сложного исправления.

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

Как понять, что тест изолирован? Он проходит при запуске в одиночку, в обратном порядке, в параллельном прогоне и на другой машине. Если хотя бы один из этих прогонов нестабилен, изоляции нет.

Источники: pytest docs, Google Research (Flaky Tests at Scale), Martin Fowler (non-determinism) — ссылки в шапке.