Флаки-тест - тот, что то зелёный, то красный без изменений в коде, - опаснее упавшего. Упавший тест честно указывает на проблему; флаки подрывает доверие ко всему набору: команда привыкает перезапускать до зелёного и вместе с шумом пропускает настоящие регрессии. Поэтому флак не 'терпят', а расследуют, и почти всегда за ним стоит конкретная утечка недетерминизма, а не мистика.
Диагностика начинается с симптома. Тест зелёный в одиночку и красный в наборе - почти наверняка общее состояние: один тест оставил после себя мутированный модуль, запись в общем объекте, незакрытый мок. Проверяется это перестановкой порядка через sequence.shuffle с зафиксированным seed: если падение воспроизводится на конкретном seed, причина в зависимости от порядка, а не в самом тесте.
Другой симптом - suite не завершается, висит после последнего теста. Это открытый хэндл: незакрытое соединение с базой, живой таймер, подписка на событие, которую забыли снять. Тест мог отработать и позеленеть, но процесс держит ресурс. Лечится это дисциплиной teardown - в afterEach/afterAll закрывают всё, что открыли, - и поиском источника через инструменты, показывающие, что именно не даёт процессу выйти.
Особая категория - падает только в CI, локально всё зелено. Почти всегда это среда: другая таймзона, из-за которой ломается тест на дату; меньше ядер, обнажающие гонку, незаметную на быстрой машине; иная локаль или порядок. Лечат это устранением зависимости от среды: фиксируют TZ в конфиге, заменяют ожидания по времени явными ожиданиями события, а не sleep, убирают неявные предположения о параллелизме.
Отдельно стоит производительность. Медленный набор - это тоже дефект: если тесты идут минутами, их перестают гонять локально, и обратная связь исчезает. Частая причина - тяжёлый setup, повторяемый в beforeEach там, где хватило бы beforeAll, или реальная база под юнит-тестами правил. Флаг --logHeapUsage показывает тесты, раздувающие память, и помогает найти утечки, из-за которых набор деградирует к концу прогона.
Ретрай - лечение симптома, а не причины, и применять его надо узко. test.retry маскирует флак: тест, зелёный со второй попытки, по-прежнему недетерминирован, просто шум спрятан. Оправдан ретрай в редких местах, которые честно нельзя сделать стабильными, - обычно в E2E, зависящих от внешней среды, - и там он держит их в карантине, а не притворяется, что проблемы нет. В юнит-тестах ретрай почти всегда прячет настоящий баг.
Общий метод тот же, что в отладке: воспроизвести, потом чинить. Недетерминизм воспроизводят детерминированно - фиксируя seed порядка и входные данные, - и только поймав стабильное падение, устраняют его источник: изолируют состояние, закрывают хэндлы, убирают зависимость от времени и среды. Флак, побеждённый воспроизведением, не возвращается; флак, замазанный ретраем, ждёт худшего момента.
Типичные провалы собраны в одну привычку - мириться с флаком. Перезапускать CI до зелёного вместо расследования; вешать test.retry на юнит, пряча гонку; оставлять sleep вместо ожидания события и удивляться падениям на загруженном раннере; держать реальную базу под тестами правил и жаловаться на медленный набор. Каждый флак имеет причину - найдите её через seed и порядок, а не глушите повторным запуском.
// vitest.config.ts -> test:
sequence: { shuffle: true, seed: 12345 }, // ловим зависимость от порядка
// afterEach закрывает всё открытое - иначе suite виснет на хэндле
afterEach(async () => {
await db.close()
vi.useRealTimers()
})