Содержание
Что такое flaky-тест
Flaky-тест (нестабильный, «флакающий») — это тест, который на неизменном коде иногда проходит, а иногда падает. Запустили — зелёный. Запустили ещё раз — красный. Ничего не меняли.
Это коварнее обычного падения. Упавший детерминированный тест честно говорит: «здесь баг». Флакающий тест врёт в обе стороны — то ложно тревожит, то ложно успокаивает.
Почему это дорого
Прямой вред — потраченное время на разбор ложных падений. Но настоящая цена выше: flaky-тесты разрушают доверие к тестам как к сигналу.
Сценарий деградации знаком многим командам:
- Тесты иногда падают «сами по себе».
- Разработчики привыкают перезапускать прогон не глядя.
- Перезапуск входит в привычку: красное — значит «наверное, флак».
- Однажды красное — это настоящий баг, но его перезапускают и катят в прод.
Как только команда начинает игнорировать красный прогон, тесты перестают выполнять свою главную работу.
Пять главных причин
По опыту, подавляющее большинство флакающих тестов сводится к пяти источникам.
1. Тайминги и асинхронность
Самая частая причина. Тест ждёт результата фиксированное время вместо реального условия.
// ПЛОХО: надеемся, что 500 мс хватит
await sleep(500);
expect(element.textContent).toBe("Загружено");
// ХОРОШО: ждём само условие
await waitFor(() => expect(element.textContent).toBe("Загружено"));
sleep — это ставка на скорость машины. На быстром ноутбуке проходит, на загруженном CI-агенте — нет. Решение: ждать конкретное состояние, а не время.
2. Общее изменяемое состояние
Тесты делят между собой что-то живое: базу, файл, глобальную переменную, кэш. Один тест оставил мусор — следующий упал.
// Глобальный счётчик, который тесты не сбрасывают
let cache = {};
test("A", () => { cache.user = 1; /* ... */ });
test("B", () => { expect(cache).toEqual({}); }); // упадёт после A
Решение: изоляция. Каждый тест поднимает и убирает своё состояние (beforeEach / afterEach), не полагаясь на чистоту от соседей.
3. Зависимость от порядка выполнения
Частный, но опасный случай предыдущего. Тесты проходят в одном порядке и падают в другом, потому что один молча готовит данные для другого.
Это особенно больно, когда раннер рандомизирует порядок или распараллеливает тесты. Хороший признак здоровья набора — он проходит при случайном порядке. Многие фреймворки умеют его перемешивать (--shuffle); включите и почините то, что всплывёт.
4. Внешние зависимости
Тест дёргает реальный сторонний сервис, сеть или системное время. Любая из этих вещей может моргнуть.
// ПЛОХО: тест зависит от реальной даты
test("скидка действует сегодня", () => {
expect(isDiscountActive()).toBe(true); // сломается после дедлайна акции
});
// ХОРОШО: время под контролем
test("скидка действует в день акции", () => {
jest.useFakeTimers().setSystemTime(new Date("2026-06-23"));
expect(isDiscountActive()).toBe(true);
});
Решение: на уровне unit и интеграции внешние зависимости мокаются или подменяются. Реальные внешние вызовы оставляйте малому числу осознанных e2e-тестов, понимая, что они будут менее стабильны.
5. Недетерминизм в самих данных
Тест полагается на порядок, который никто не гарантировал. Классика — проверка коллекции, которая на самом деле неупорядочена.
// ПЛОХО: порядок строк из БД не гарантирован без ORDER BY
expect(users).toEqual([alice, bob]);
// ХОРОШО: сравниваем без учёта порядка
expect(users).toEqual(expect.arrayContaining([alice, bob]));
expect(users).toHaveLength(2);
Сюда же — зависимость от хеш-порядка ключей, от случайных id, от локали при сортировке строк.
Как ловить флак системно
Точечно чинить мало — нужен процесс, который их выявляет.
Запускайте подозрительный тест в цикле. Прогон 50–100 раз подряд быстро отделяет стабильный тест от флакающего:
for i in $(seq 1 100); do npm test -- suspect.test.js || break; done
Рандомизируйте порядок в CI постоянно, а не только при отладке. Это превентивно ловит зависимости между тестами.
Логируйте каждый ретрай. Если CI перезапускает упавшие тесты, эти перезапуски должны быть видны в отчёте, а не молча прятаться. Иначе флак копится незаметно.
Заведите карантин. Хронически нестабильный тест выносится из блокирующего прогона в отдельную группу — но остаётся в бэклоге как обязательная починка. Это лучше двух крайностей: и не блокирует команду ложными падениями, и не теряет покрытие через тихое удаление.
Чего НЕ делать
Не маскируйте ретраями. Автоматический перезапуск до трёх раз превращает красное в зелёное и прячет проблему. Как аварийный клапан на критичном пайплайне — допустимо, но только вместе с логированием и разбором. Как постоянное решение — это способ накопить технический долг.
Не добавляйте sleep побольше. Увеличение задержки не лечит race condition, а лишь снижает частоту падений — до момента, когда машина окажется ещё медленнее.
Не удаляйте тест молча. Потеря покрытия без следа опаснее самого флака: вы перестаёте проверять реальную функциональность и даже не знаете об этом.
Итог
Flaky-тест — это не случайность, а недиагностированная причина: тайминг, общее состояние, порядок, внешний вызов или недетерминизм данных. Чините источник, а не симптом. Главная ставка — доверие: набор тестов ценен ровно настолько, насколько команда верит красному прогону. Один проигнорированный флак стоит дешевле, чем привычка игнорировать красное.
Обсуждение
Пока нет комментариев. Будьте первым.