Sleepy Test.
A Sleepy Test pauses execution with a hard-coded delay (`Thread.sleep`, `setTimeout`, `cy.wait(2000)`, `page.waitForTimeout`) to wait for asynchronous work instead of waiting on the actual condition.
##Signs and Symptoms
You see a magic-number delay sitting between an action and its assertion, with a comment apologizing for it:
test('order is processed', async () => {
submitOrder(order);
// give the worker time to finish
await new Promise((r) => setTimeout(r, 2000));
expect(await getOrderStatus(order.id)).toBe('processed');
});
Tell-tale signs:
- Fixed sleeps in test bodies or hooks:
Thread.sleep(500),await sleep(1000),time.sleep(2),cy.wait(3000),await page.waitForTimeout(2000),browser.pause(2000). - Comments like
// wait for the animation,// let the DB catch up,// flaky without this. - The sleep duration drifts upward over time (
1000becomes2000becomes5000) as people fight intermittent failures by padding the delay. - The number is the only synchronization — there is no assertion, poll, or event that the wait is actually keyed to.
- A slow suite where the wall-clock runtime is dominated by sleeps rather than real work.
The smell is about unconditional waiting. cy.wait('@apiAlias') or await expect(locator).toBeVisible() wait on a condition and are fine; cy.wait(2000) and page.waitForTimeout(2000) wait on the clock and are not.
##Reasons for the Problem
Why it happens
- An asynchronous operation (a queue, a timer, an animation, a network call, a background thread) has no obvious hook to wait on, so a delay is the path of least resistance.
- A test was intermittently failing and someone "fixed" it by inserting or lengthening a sleep until it went green on their machine.
- The test interacts with a real external system (clock, scheduler, filesystem, HTTP service) whose completion isn't observable, so timing is guessed.
Why it hurts
- Reliability / false confidence. A sleep encodes an assumption — "the work finishes within N ms." Processing time varies across machines, CI load, and runs, so the test is non-deterministic: it passes locally and fails on a loaded CI agent, or worse, the sleep is too short and the assertion races the code, passing only because of timing luck. This is one of the most-cited root causes of flaky tests.
- Slowness. A fixed sleep always waits the full duration, even when the work finished in 20 ms. Multiplied across a suite, sleeps turn seconds of real work into minutes of idle waiting, which discourages running tests often.
- Maintainability. The magic number is fragile: speeding up or slowing down the system under test silently breaks the timing contract, and the only "fix" people reach for is bumping the number — a ratchet that makes the suite slower and still flaky.
- Readability. The delay hides what the test is actually waiting for. A reader can't tell whether
sleep(2000)guards a DB write, a render, or nothing at all, so the test's intent is obscured.
##Treatment
Replace "wait for a fixed time" with "wait for the condition." Identify the observable signal that the async work is done, then block on that with a generous timeout.
- Poll for the condition. Use a polling/retry helper — Awaitility (JVM),
vi.waitFor/waitFor(Vitest, Testing Library), Jest'swaitFor, or your test runner's auto-retrying assertions — so the test proceeds the instant the condition is true and only fails after a timeout. - Prefer built-in awaited assertions. Web E2E tools already retry: Playwright's web-first assertions (
await expect(locator).toBeVisible()) and Cypress's auto-retrying commands remove the need to wait at all. - Wait on events, not the clock. Await the promise,
await-the network response, or use aCountDownLatch/callback/waitFor('@alias')that the production code actually signals. - Control time instead of spending it. When the delay is a real timer in the code under test, use fake timers (
jest.useFakeTimers(),vi.useFakeTimers(),vi.advanceTimersByTimeAsync) so you advance the clock deterministically rather than sleeping.
Before:
test('order is processed', async () => {
submitOrder(order);
await new Promise((r) => setTimeout(r, 2000)); // sleepy
expect(await getOrderStatus(order.id)).toBe('processed');
});
After (poll the condition with a bounded timeout):
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 equivalents:
// Playwright — web-first assertion auto-retries until visible or timeout
await expect(page.getByText('processed')).toBeVisible();
// Cypress — wait on the request, not a number
cy.intercept('POST', '/orders').as('createOrder');
cy.wait('@createOrder');
The result waits no longer than necessary, fails fast with a clear timeout message, and no longer depends on the speed of the machine.
##Detected by
- sonar java:S2925 — "Thread.sleep" should not be used in tests
- eslint-plugin-playwright playwright/no-wait-for-timeout — no-wait-for-timeout (disallow page.waitForTimeout)
- eslint-plugin-cypress cypress/no-unnecessary-waiting — no-unnecessary-waiting (disallow cy.wait with a number)
- eslint-plugin-ui-testing ui-testing/no-hard-wait — no-hard-wait (cy.wait, page.waitForTimeout, browser.pause, t.wait)