ConstructiCat Logo
CodeBust.
Browse section ▾

Спящий тест.

Спящий тест приостанавливает выполнение захардкоженной задержкой (`Thread.sleep`, `setTimeout`, `cy.wait(2000)`, `page.waitForTimeout`), чтобы дождаться асинхронной работы, вместо того чтобы ждать само нужное условие.

##Signs and Symptoms

Вы видите задержку с «магическим числом», сидящую между действием и его проверкой, с комментарием, извиняющимся за неё:

test('order is processed', async () => {
  submitOrder(order);

  // дать воркеру время завершиться
  await new Promise((r) => setTimeout(r, 2000));

  expect(await getOrderStatus(order.id)).toBe('processed');
});

Выдающие признаки:

  • Фиксированные паузы в телах тестов или хуках: Thread.sleep(500), await sleep(1000), time.sleep(2), cy.wait(3000), await page.waitForTimeout(2000), browser.pause(2000).
  • Комментарии вроде // ждём анимацию, // дать БД догнать, // без этого нестабильно.
  • Длительность паузы со временем ползёт вверх (1000 становится 2000, становится 5000), пока люди борются с эпизодическими падениями, наращивая задержку.
  • Число — единственная синхронизация: нет ни проверки, ни опроса, ни события, к которому это ожидание на самом деле привязано.
  • Медленный набор, где реальное время прогона определяется паузами, а не реальной работой.

Запах — о безусловном ожидании. cy.wait('@apiAlias') или await expect(locator).toBeVisible() ждут условие и это нормально; cy.wait(2000) и page.waitForTimeout(2000) ждут по часам и это не нормально.

##Reasons for the Problem

Почему это происходит

  • У асинхронной операции (очередь, таймер, анимация, сетевой вызов, фоновый поток) нет очевидной зацепки, на которой можно подождать, поэтому задержка — путь наименьшего сопротивления.
  • Тест время от времени падал, и кто-то «починил» его, вставив или удлинив паузу, пока он не позеленел на его машине.
  • Тест взаимодействует с реальной внешней системой (часы, планировщик, файловая система, HTTP-сервис), завершение которой не наблюдаемо, поэтому время подбирается наугад.

Почему это вредно

  • Надёжность / ложная уверенность. Пауза кодирует допущение — «работа завершится за N мс». Время обработки варьируется между машинами, нагрузкой CI и прогонами, поэтому тест недетерминирован: он проходит локально и падает на загруженном агенте CI или, хуже того, пауза слишком коротка и проверка состязается с кодом, проходя лишь по счастливому стечению таймингов. Это одна из самых упоминаемых коренных причин нестабильных (flaky) тестов.
  • Медленность. Фиксированная пауза всегда ждёт всю длительность, даже когда работа завершилась за 20 мс. Умноженные на весь набор, паузы превращают секунды реальной работы в минуты простоя, что отбивает желание запускать тесты часто.
  • Сопровождаемость. «Магическое число» хрупко: ускорение или замедление проверяемой системы тихо ломает временной контракт, и единственная «починка», к которой прибегают, — увеличить число; этот храповик делает набор медленнее и всё равно нестабильным.
  • Читаемость. Задержка скрывает, чего тест на самом деле ждёт. Читатель не может сказать, охраняет ли sleep(2000) запись в БД, отрисовку или вообще ничего, поэтому намерение теста затемнено.

##Treatment

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

  1. Опрашивайте условие. Используйте помощник опроса/повтора — Awaitility (JVM), vi.waitFor / waitFor (Vitest, Testing Library), waitFor из Jest или авто-повторяющиеся проверки вашего раннера, — чтобы тест продолжился в тот же миг, как условие станет истинным, и падал лишь по истечении таймаута.
  2. Предпочитайте встроенные ожидающие проверки. Веб-инструменты E2E уже повторяют: web-first проверки Playwright (await expect(locator).toBeVisible()) и авто-повторяющиеся команды Cypress вовсе устраняют нужду ждать.
  3. Ждите события, а не часы. Дождитесь промиса, выполните await сетевого ответа или используйте CountDownLatch/колбэк/waitFor('@alias'), который продакшен-код действительно сигнализирует.
  4. Управляйте временем вместо того, чтобы его тратить. Когда задержка — реальный таймер в проверяемом коде, используйте поддельные таймеры (jest.useFakeTimers(), vi.useFakeTimers(), vi.advanceTimersByTimeAsync), чтобы детерминированно продвигать часы, а не спать.

До:

test('order is processed', async () => {
  submitOrder(order);
  await new Promise((r) => setTimeout(r, 2000)); // сонно
  expect(await getOrderStatus(order.id)).toBe('processed');
});

После (опрос условия с ограниченным таймаутом):

import { vi } from 'vitest';

test('order is processed', async () => {
  submitOrder(order);
  await vi.waitFor(
    async () => expect(await getOrderStatus(order.id)).toBe('processed'),
    { timeout: 5000, interval: 50 },
  );
});

Эквиваленты для Playwright/Cypress:

// Playwright — web-first проверка автоматически повторяется до видимости или таймаута
await expect(page.getByText('processed')).toBeVisible();

// Cypress — ждём запрос, а не число
cy.intercept('POST', '/orders').as('createOrder');
cy.wait('@createOrder');

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

##Detected by