Спящий тест.
Спящий тест приостанавливает выполнение захардкоженной задержкой (`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
Замените «ждать фиксированное время» на «ждать условие». Определите наблюдаемый сигнал того, что асинхронная работа завершена, и блокируйтесь на нём с щедрым таймаутом.
- Опрашивайте условие. Используйте помощник опроса/повтора — Awaitility (JVM),
vi.waitFor/waitFor(Vitest, Testing Library),waitForиз Jest или авто-повторяющиеся проверки вашего раннера, — чтобы тест продолжился в тот же миг, как условие станет истинным, и падал лишь по истечении таймаута. - Предпочитайте встроенные ожидающие проверки. Веб-инструменты E2E уже повторяют: web-first проверки Playwright (
await expect(locator).toBeVisible()) и авто-повторяющиеся команды Cypress вовсе устраняют нужду ждать. - Ждите события, а не часы. Дождитесь промиса, выполните
awaitсетевого ответа или используйтеCountDownLatch/колбэк/waitFor('@alias'), который продакшен-код действительно сигнализирует. - Управляйте временем вместо того, чтобы его тратить. Когда задержка — реальный таймер в проверяемом коде, используйте поддельные таймеры (
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
- sonar java:S2925 — «Thread.sleep» не должен использоваться в тестах
- eslint-plugin-playwright playwright/no-wait-for-timeout — no-wait-for-timeout (запрет page.waitForTimeout)
- eslint-plugin-cypress cypress/no-unnecessary-waiting — no-unnecessary-waiting (запрет cy.wait с числом)
- eslint-plugin-ui-testing ui-testing/no-hard-wait — no-hard-wait (cy.wait, page.waitForTimeout, browser.pause, t.wait)