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

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

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

Почему случайный перезапуск без учёта это худшая из реакций

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

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

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

Перезапуск с учётом как способ отличить мерцание от настоящего бага

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

# перезапуск с учётом: повтор разрешён, но факт мерцания фиксируется
retries: 2
on_flake:
  - record: { test, attempts, error, timestamp }
  - notify: owner
  - do_not_silently_pass: true

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

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

Карантин как изоляция, которая обязательно имеет владельца и срок

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

# карантин через тег, исключение из блокирующего прогона
pytest -m "not flaky"        # основной пайплайн пропускает мерцающие
pytest -m "flaky" --no-block # карантинные гоняются, но не блокируют

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

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

Корневые причины нестабильности и как чинить, а не латать

Карантин это обезболивающее, а не лечение. Настоящее устранение требует докопаться до корневой причины, и причины эти образуют узнаваемый набор. Самая частая это тайминг: тест проверяет результат раньше, чем интерфейс его отрисовал, или раньше, чем пришёл ответ сети. Лечится это ожиданием конкретного состояния вместо фиксированной паузы. Вторая причина это гонки и асинхронность, когда порядок завершения операций непредсказуем. Третья это зависимость от порядка тестов, когда один тест оставляет состояние, ломающее следующий. Четвёртая это изменчивость внешних сервисов и сети, лечимая подменой ответов через моки. Пятая это зависимость от среды, времени, случайных значений.

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

# сбор улик на каждое падение для анализа причины
on_failure:
  capture: [logs, screenshots, video, traces, console]
  store: central   # переживает перезапуск задачи

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

Измерение частоты мерцания и пороги для автоматических действий

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

# пример отслеживания частоты мерцания за неделю
тест                        прогонов  падений  частота  тренд
checkout_full_flow              420      18      4.3%    растёт
login_with_2fa                  410       2      0.5%    стабилен
search_autocomplete             430       0      0.0%    починен

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

Полезно не считать тест починенным по одному успешному прогону. После исправления частоту продолжают отслеживать, и лишь устойчивый ноль на протяжении заданного числа прогонов подтверждает, что причина устранена, а не затаилась. Непрерывный мониторинг трендов вскрывает и регрессии: тест, годами стабильный, вдруг начал мерцать после изменения, и метрика поймает это раньше, чем мерцание отравит пайплайн. Так измерение замыкает весь цикл от обнаружения до подтверждения починки.

Профилактика на уровне фреймворка и предотвращение до слияния

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

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

Чтобы свести стратегию к рабочим ориентирам, ниже единственный список этой статьи, расставленный по важности:

  1. Никогда не перезапускать молча, а использовать перезапуск с учётом, фиксирующий каждое мерцание в базу с именем, частотой и ссылкой на падение;
  2. Маршрутизировать по картине падений: рассыпанные по разным тестам это нестабильность тестов, стена падений по всему набору это инфраструктура;
  3. Изолировать мерцающие тесты карантином, но обязательно с владельцем, тикетом и сроком, иначе карантин становится вечным кладбищем покрытия;
  4. Докапываться до корневой причины из узнаваемого набора, собирая улики централизованно, а не латать симптомы повторами;
  5. Чинить по одному тесту за полный цикл, воспроизводя нестабильность локально через имитацию условий сборки;
  6. Вкладываться в профилактику: фреймворк со встроенным ожиданием против тайминга и проверку частоты мерцания на пулреквесте до слияния.

Эти шесть правил превращают нестабильность из хаоса в управляемый процесс. Цель тут не достичь нулевого мерцания, его не бывает, а научиться им управлять: обнаруживать рано, чинить что можно, изолировать остальное под надзором.

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