Содержание

Что такое flaky-тест

Flaky-тест (нестабильный, «флакающий») — это тест, который на неизменном коде иногда проходит, а иногда падает. Запустили — зелёный. Запустили ещё раз — красный. Ничего не меняли.

Это коварнее обычного падения. Упавший детерминированный тест честно говорит: «здесь баг». Флакающий тест врёт в обе стороны — то ложно тревожит, то ложно успокаивает.

Почему это дорого

Прямой вред — потраченное время на разбор ложных падений. Но настоящая цена выше: flaky-тесты разрушают доверие к тестам как к сигналу.

Сценарий деградации знаком многим командам:

  1. Тесты иногда падают «сами по себе».
  2. Разработчики привыкают перезапускать прогон не глядя.
  3. Перезапуск входит в привычку: красное — значит «наверное, флак».
  4. Однажды красное — это настоящий баг, но его перезапускают и катят в прод.

Как только команда начинает игнорировать красный прогон, тесты перестают выполнять свою главную работу.

Пять главных причин

По опыту, подавляющее большинство флакающих тестов сводится к пяти источникам.

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-тест — это не случайность, а недиагностированная причина: тайминг, общее состояние, порядок, внешний вызов или недетерминизм данных. Чините источник, а не симптом. Главная ставка — доверие: набор тестов ценен ровно настолько, насколько команда верит красному прогону. Один проигнорированный флак стоит дешевле, чем привычка игнорировать красное.

Поделиться: