ConstructiCat Logo
CodeBust.
Browse section ▾

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 (1000 becomes 2000 becomes 5000) 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.

  1. Poll for the condition. Use a polling/retry helper — Awaitility (JVM), vi.waitFor / waitFor (Vitest, Testing Library), Jest's waitFor, or your test runner's auto-retrying assertions — so the test proceeds the instant the condition is true and only fails after a timeout.
  2. 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.
  3. Wait on events, not the clock. Await the promise, await-the network response, or use a CountDownLatch/callback/waitFor('@alias') that the production code actually signals.
  4. 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