---
title: "Неустойчивый (нестабильный, flaky) тест"
type: "test-smell"
slug: "erratic-test"
url: "http://localhost:3000/ru/test-smells/erratic-test.md"
category: "Запахи нестабильности"
description: "Неустойчивый (flaky) тест на неизменном коде то проходит, то падает, потому что его исход зависит от тайминга, порядка выполнения, общего состояния или иных недетерминированных факторов, а не от проверяемого поведения."
---
# Неустойчивый (нестабильный, flaky) тест

> Неустойчивый (flaky) тест на неизменном коде то проходит, то падает, потому что его исход зависит от тайминга, порядка выполнения, общего состояния или иных недетерминированных факторов, а не от проверяемого поведения.

## Signs and Symptoms

Тест, который **на одном запуске зелёный, а на следующем красный без каких-либо изменений кода**, является неустойчивым. Распознать его можно по поведению людей вокруг него не меньше, чем по коду: разработчики перезапускают CI, «чтобы прошло», добавляют `@Flaky`/`retry(3)` или помещают тест в карантин вместо того, чтобы починить.

Типичные приметы в коде и в характере падений:

* **Тайминг/асинхронность:** «ожидания» через `sleep`/`setTimeout`, недождавшиеся промисы или проверки, состязающиеся с тестируемой системой. Падения коррелируют со скоростью машины или нагрузкой на CI.
* **Зависимость от порядка:** тест проходит в изоляции, но падает в полном наборе, или наоборот. Перемешивание порядка тестов меняет результат.
* **Разделяемое изменяемое состояние:** статические коллекции, переиспользуемая строка в БД, синглтон или фикстура, изменённая более ранним тестом.
* **Недетерминированные входные данные:** `Date.now()`/`new Date()`, `Math.random()`, локаль/часовой пояс, порядок обхода хеш-таблицы или автогенерируемые идентификаторы.
* **Внешние ресурсы:** реальная сеть, файловая система или часы. Падения выглядят как таймауты или «connection refused», а не как несовпадения в проверках.
* **Условные проверки:** `expect` спрятан внутри `if`/`catch`/коллбэков, поэтому тест молча проходит, когда ветка не выполняется.

```js
// Запах: захардкоженное «ожидание», общий массив и реальные часы
const created = [];                       // общий для тестов → interacting tests

test('shows a fresh receipt', async () => {
  render(<Checkout />);
  fireEvent.click(screen.getByText('Pay'));
  await new Promise(r => setTimeout(r, 300));   // надеемся, что запрос завершился
  created.push('order-1');                       // протекает в последующие тесты
  expect(screen.getByRole('status'))
    .toHaveTextContent(`Paid ${new Date().toISOString()}`); // меняется на каждом запуске
});

```

Это напрямую соответствует подзапахам _Erratic Test_ по Мезаросу: **Interacting Tests / Test Run War** (общее состояние), **Lonely Test** (запускается только после другого), **Resource Optimism** (предполагает, что внешний ресурс на месте), **Resource Leakage** (не убирает за собой) и **Nondeterministic / Unrepeatable Test** (время, случайность, асинхронность).

## Reasons for the Problem

**Почему так происходит**

* **Неявный тайминг.** Асинхронный код «синхронизируют» фиксированными `sleep`\-ами либо вообще не дожидаются его. Задержка — это догадка: сегодня её хватает, а завтра под нагрузкой она окажется слишком короткой.
* **Скрытая связанность.** Тесты разделяют базу данных, синглтон, переменные уровня модуля или файлы на диске. Побочные эффекты одного теста становятся предусловиями другого (по Мезаросу — _Interacting Tests_ / _Test Run War_), поэтому результат зависит от порядка и от того, что выполнялось параллельно.
* **Просачиваются недетерминированные входные данные.** Реальное системное время, `Math.random()`, локаль/часовой пояс и неупорядоченные коллекции меняются от запуска к запуску и от машины к машине.
* **Оптимистичная зависимость от окружения.** Тесты обращаются к живой сети/сервису или предполагают, что файл существует (_Resource Optimism_), и никогда не освобождают захваченное (_Resource Leakage_).
* **Проверки, которые можно пропустить.** Если `expect` помещён в условие или в `.catch`, проверка может вообще не выполниться, и тест «проходит» по случайности.

**Чем это вредит**

* **Ложная уверенность и замаскированные дефекты.** Нестабильный тест может упасть по причинам, не связанным с продакшеном, _и_ настоящая регрессия может спрятаться за падением, которое все списывают «просто на флаки». Сигнал больше не отличить от шума.
* **Подрыв доверия.** Как только за набором тестов закрепляется репутация нестабильного, разработчики игнорируют красные сборки и рефлекторно перезапускают их, что приучает команду полностью пренебрегать набором тестов.
* **Потерянное время и сломанные пайплайны.** Повторы, перезапуски и расследования «это я или тест?» тормозят всех и блокируют CI/CD на пустом месте.
* **Плохая сопровождаемость.** Неустойчивые тесты трудно отлаживать, потому что падение невоспроизводимо; их склонны отключать, а не чинить, тихо снижая реальное покрытие.

## Treatment

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

**1\. Замените sleep-ы ожиданием по условию.** Опрашивайте именно то состояние, которое вам важно, вместо того чтобы угадывать длительность.

```js
// до — состязательная фиксированная задержка
fireEvent.click(screen.getByText('Pay'));
await new Promise(r => setTimeout(r, 300));
expect(screen.getByRole('status')).toBeInTheDocument();

// после — дождаться условия, затем проверить
fireEvent.click(screen.getByText('Pay'));
expect(await screen.findByRole('status')).toBeInTheDocument();

```

**2\. Сделайте входные данные детерминированными.** Внедряйте часы или используйте фейковые таймеры; задавайте seed или заглушку для случайности; фиксируйте локаль/часовой пояс.

```js
// до: зависит от реальной даты
expect(label).toBe(`Paid ${new Date().toISOString()}`);

// после: заморозить время (vitest/jest)
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-06-15T00:00:00Z'));

```

**3\. Изолируйте каждый тест.** Давайте каждому тесту свежие фикстуры, сбрасывайте общее состояние (БД, синглтоны, глобальные переменные модуля) в `beforeEach`/`afterEach` и избегайте изменяемых переменных уровня модуля. Запускайте набор в **случайном порядке** (например, `--seed`/рандомизация в Jest, `MethodOrderer.Random` в JUnit), чтобы рано выявлять зависимости от порядка.

**4\. Заглушайте внешние ресурсы.** Мокайте сеть, файловую систему и системные вызовы, чтобы тест никогда не зависел от доступности сервиса (лечит _Resource Optimism_); всегда освобождайте/убирайте захваченные ресурсы (лечит _Resource Leakage_).

**5\. Дожидайтесь всей асинхронной работы и проверяйте безусловно.** Возвращайте/дожидайтесь промисов и асинхронных тестовых утилит; выносите побочные эффекты из коллбэков `waitFor`, чтобы они выполнялись один раз; никогда не закапывайте `expect` внутрь `if`/`catch`.

**6\. Проверьте исправление.** Прогоните теперь уже детерминированный тест много раз (а также под нагрузкой / в перемешанном порядке), чтобы подтвердить стабильность, прежде чем выводить его из карантина.

## Detected by

- **sonar** `java:S5973` — Tests should be stable (https://rules.sonarsource.com/java/RSPEC-5973/)
- **sonar** `java:S2925` — "Thread.sleep" should not be used in tests (https://rules.sonarsource.com/java/RSPEC-2925/)
- **eslint-jest** `no-conditional-expect` — no-conditional-expect (https://github.com/jest-community/eslint-plugin-jest/blob/main/docs/rules/no-conditional-expect.md)
- **eslint-vitest** `no-conditional-expect` — no-conditional-expect (https://github.com/veritem/eslint-plugin-vitest/blob/main/docs/rules/no-conditional-expect.md)
- **eslint-testing-library** `await-async-queries` — await-async-queries (https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/await-async-queries.md)
- **eslint-testing-library** `await-async-utils` — await-async-utils (https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/await-async-utils.md)
- **eslint-testing-library** `no-wait-for-side-effects` — no-wait-for-side-effects (https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-wait-for-side-effects.md)
