Resource library

QA How-To

Jest fake timers: A QA Guide (2026)

Master Jest fake timers qa patterns for delays, intervals, dates, promises, debounce logic, cleanup, and deterministic async tests with current Jest APIs.

21 min read | 2,737 words

TL;DR

Jest fake timers replace real clock and timer APIs so a test can control delays without sleeping. Enable them in setup, schedule the subject, advance with the narrowest matching API, await async variants when promises are involved, and restore real timers after each test.

Key Takeaways

  • Install fake timers before scheduling the work you intend to control.
  • Advance only the amount or next timer needed to prove the behavior.
  • Use async timer APIs when timer callbacks schedule promises that affect assertions.
  • Set system time for calendar logic, but advance timers separately to fire callbacks.
  • Use pending-timer controls for recursive intervals and polling loops.
  • Restore real timers and mocks in cleanup so tests stay isolated.
  • Prefer an injected clock for complex domain time while using fake timers at integration boundaries.

Jest fake timers qa tests replace real time with a controllable clock, allowing a one-hour expiration, retry delay, debounce window, or interval tick to execute instantly and deterministically. The essential pattern is: enable fake timers before scheduling work, trigger the subject, advance the clock with the narrowest suitable API, assert the state, and restore real timers.

Fake timers remove waiting, but they do not remove asynchronous semantics. Promise continuations, recursive scheduling, system date changes, UI rendering, and cleanup each need deliberate handling. This guide uses current Jest APIs and runnable examples to make those boundaries clear.

TL;DR

Goal Jest API When to use it
Fake time and timers jest.useFakeTimers() Before subject scheduling
Advance a duration jest.advanceTimersByTime(ms) Exact delay or interval behavior
Advance with promise work await jest.advanceTimersByTimeAsync(ms) Timer callback affects async state
Run current queue jest.runOnlyPendingTimers() Recursive timers or cleanup
Run all finite timers jest.runAllTimers() Known finite timer graph
Move to one next deadline jest.advanceTimersToNextTimer() Assert staged scheduling
Set wall-clock date jest.setSystemTime(date) Expiry and calendar decisions
Inspect queue jest.getTimerCount() Leak diagnostics and assertions
Restore environment jest.useRealTimers() Test cleanup

Avoid real sleeps. They slow suites and still race overloaded CI workers. Control the clock, then assert both what happened and what has not happened yet.

1. Jest fake timers qa: The Mental Model

Jest's modern fake timers replace selected global timing APIs with implementations backed by a virtual clock. These include common timer functions and clock-related APIs such as Date, subject to environment and doNotFake configuration. Calling jest.advanceTimersByTime(1000) moves virtual time and executes due timer callbacks without waiting one real second.

Three concepts prevent most mistakes. First, installation timing matters. A timeout created before jest.useFakeTimers() belongs to the real environment and will not move with the virtual clock. Second, clock advancement runs scheduled callback work according to the selected API, but promise work may require an async timer method. Third, changing system time and advancing timer deadlines are separate intentions.

Fake time is global within the test environment, so it can affect libraries that read Date.now(), schedule background cleanup, use requestAnimationFrame, or measure performance. Scope it to tests that need it. If an entire file tests scheduling, lifecycle hooks are reasonable. If only one case needs a fixed date, enable it locally.

The goal is not to simulate the operating system perfectly. It is to make observable scheduling behavior testable. A good test proves the callback does not run before the deadline, runs at the deadline, produces the correct state, and leaves no unexpected timer behind.

2. Set Up and Tear Down Timers Correctly

Install Jest and create a script in a test project if needed:

npm install --save-dev jest
npm pkg set scripts.test=jest

For ESM tests, import Jest APIs explicitly. The following pattern scopes fake timers to each test and restores the environment afterward.

import {afterEach, beforeEach, expect, jest, test} from '@jest/globals';

beforeEach(() => {
  jest.useFakeTimers();
});

afterEach(() => {
  jest.clearAllTimers();
  jest.restoreAllMocks();
  jest.useRealTimers();
});

test('virtual time starts under test control', () => {
  const callback = jest.fn();
  setTimeout(callback, 1000);

  jest.advanceTimersByTime(999);
  expect(callback).not.toHaveBeenCalled();

  jest.advanceTimersByTime(1);
  expect(callback).toHaveBeenCalledTimes(1);
});

Some integration tests should run pending timers before restoring real timers because a library scheduled cleanup that otherwise disappears. Use jest.runOnlyPendingTimers() before useRealTimers() when executing those callbacks is the intended cleanup. In other suites, running pending application work could cause unwanted side effects, so explicitly clearing timers is safer. Choose based on ownership, not ritual.

Do not alternate real and fake timers within one assertion flow unless the transition is the behavior being tested. Restoring real timers does not magically translate a virtual future timeout into a real one. Keep setup, scheduling, advancement, and assertions in a coherent virtual-time phase.

3. Select the Right Timer Advancement API

runAllTimers() is tempting because it empties a finite timer graph in one call. It can also hide an incorrect delay or explode on a recursive scheduler. Prefer advanceTimersByTime() when duration is part of the requirement and advanceTimersToNextTimer() when ordering matters more than the numeric delay.

Consider a two-stage notification:

// reminder.js
export function scheduleReminder(send) {
  setTimeout(() => send('warning'), 5_000);
  setTimeout(() => send('expired'), 10_000);
}
import {expect, jest, test} from '@jest/globals';
import {scheduleReminder} from './reminder.js';

test('warns before expiration', () => {
  jest.useFakeTimers();
  const send = jest.fn();

  scheduleReminder(send);
  expect(jest.getTimerCount()).toBe(2);

  jest.advanceTimersByTime(4_999);
  expect(send).not.toHaveBeenCalled();

  jest.advanceTimersByTime(1);
  expect(send).toHaveBeenLastCalledWith('warning');

  jest.advanceTimersByTime(5_000);
  expect(send).toHaveBeenLastCalledWith('expired');
  expect(send).toHaveBeenCalledTimes(2);

  jest.useRealTimers();
});

This proves boundaries that runAllTimers() would skip. runOnlyPendingTimers() executes timers currently pending without recursively exhausting newly created timers, making it suitable for self-rescheduling work. Async counterparts exist for timer operations that need promise callbacks to run before the API resolves.

For requestAnimationFrame logic in a suitable environment, Jest also provides advanceTimersToNextFrame(). Use it when frame scheduling is the contract rather than assuming a hard-coded duration in every test.

4. Test Promises Inside Timer Callbacks

A timer callback can launch asynchronous work. The synchronous advancement method executes the callback, but the promise continuation may not have updated state when the next assertion runs. Current Jest provides async variants such as advanceTimersByTimeAsync() and runAllTimersAsync() to let scheduled promise callbacks run before completion.

// delayed-status.js
export function delayedStatus(loadStatus, delayMs) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      loadStatus().then(resolve, reject);
    }, delayMs);
  });
}
import {expect, jest, test} from '@jest/globals';
import {delayedStatus} from './delayed-status.js';

test('resolves after the configured delay', async () => {
  jest.useFakeTimers();
  const loadStatus = jest.fn().mockResolvedValue('ready');

  const resultPromise = delayedStatus(loadStatus, 2_000);
  expect(loadStatus).not.toHaveBeenCalled();

  await jest.advanceTimersByTimeAsync(2_000);

  await expect(resultPromise).resolves.toBe('ready');
  expect(loadStatus).toHaveBeenCalledTimes(1);
  jest.useRealTimers();
});

Create the result promise before advancing, but do not immediately await it. Awaiting a promise whose resolution depends on an unadvanced fake timer deadlocks the test until Jest's outer timeout. Advance first, then await the result.

Not every async test needs fake timers. If the promise resolves without time-based scheduling, ordinary await is clearer. Fake timers should control a timer boundary, not become a generic cure for every asynchronous test. For wider patterns, see JavaScript async testing best practices.

5. Freeze Calendar Time With setSystemTime

Use jest.setSystemTime() when production code reads the current date or timestamp. This supports deterministic expiration, billing cutoff, token age, and date-label tests. Enable fake timers first, then set an explicit timestamp with a time zone.

// token.js
export function isExpired(expiresAt) {
  return Date.now() >= new Date(expiresAt).getTime();
}
import {expect, jest, test} from '@jest/globals';
import {isExpired} from './token.js';

test('expires at the exact instant', () => {
  jest.useFakeTimers();
  jest.setSystemTime(new Date('2026-07-13T09:00:00.000Z'));

  expect(isExpired('2026-07-13T09:00:00.001Z')).toBe(false);
  expect(isExpired('2026-07-13T09:00:00.000Z')).toBe(true);

  jest.useRealTimers();
});

Setting system time changes what clock reads return. It does not itself fire timers because a new wall-clock value was selected. Advance the timer clock with a timer API when callbacks must execute. Keeping these operations separate makes the test intention obvious.

Always include a zone in timestamp literals. A bare date or local date-time can parse differently or cross a daylight-saving transition. For business calendars, inject a clock and a time-zone-aware domain policy rather than scattering new Date() across functions. Jest fake timers can then set the integration clock, while unit tests pass precise instants directly.

jest.getRealSystemTime() exists when diagnostics need the actual wall clock while timers are faked. Avoid using it in business assertions, since that reintroduces nondeterminism.

6. Test Intervals, Polling, and Recursive Timers

Intervals and recursive timeouts can create an infinite queue. jest.runAllTimers() protects itself with a timer execution limit, but reaching that guard means the test selected the wrong control. Advance a known number of periods or run only the next pending layer.

// poller.js
export function startPoller(check, intervalMs) {
  const id = setInterval(check, intervalMs);
  return () => clearInterval(id);
}
import {expect, jest, test} from '@jest/globals';
import {startPoller} from './poller.js';

test('polls twice and stops', () => {
  jest.useFakeTimers();
  const check = jest.fn();

  const stop = startPoller(check, 1_000);
  jest.advanceTimersByTime(2_000);
  expect(check).toHaveBeenCalledTimes(2);

  stop();
  expect(jest.getTimerCount()).toBe(0);
  jest.advanceTimersByTime(5_000);
  expect(check).toHaveBeenCalledTimes(2);

  jest.useRealTimers();
});

For recursive setTimeout, runOnlyPendingTimers() runs the current scheduled callback, which may schedule the next one without causing Jest to follow the chain forever. Assert the queue count after each stage. A polling design should expose cancellation and stop on success, error, or deadline. Fake timers make leaks visible because getTimerCount() can reveal pending work at test end.

Do not assert only how many times setInterval was called. That verifies an implementation detail without proving polling behavior. Assert the check results, stop conditions, and absence of later calls after cancellation.

7. Test Debounce, Throttle, and Retry Backoff

Debounce behavior is a strong fake-timer use case. The requirement is normally that repeated calls reset one deadline and only the latest value is delivered. Test both the silence before the boundary and the single action at the boundary.

// debounce.js
export function debounce(fn, waitMs) {
  let timerId;
  return (...args) => {
    clearTimeout(timerId);
    timerId = setTimeout(() => fn(...args), waitMs);
  };
}
import {expect, jest, test} from '@jest/globals';
import {debounce} from './debounce.js';

test('emits only the latest value after quiet time', () => {
  jest.useFakeTimers();
  const search = jest.fn();
  const debouncedSearch = debounce(search, 300);

  debouncedSearch('p');
  jest.advanceTimersByTime(200);
  debouncedSearch('play');
  jest.advanceTimersByTime(299);
  expect(search).not.toHaveBeenCalled();

  jest.advanceTimersByTime(1);
  expect(search).toHaveBeenCalledTimes(1);
  expect(search).toHaveBeenCalledWith('play');
  jest.useRealTimers();
});

For retry backoff, assert each scheduled delay and final stop condition. Inject the operation so the test can return rejection, rejection, then success. Avoid runAllTimers() if it would make a buggy unbounded retry loop expensive or obscure intermediate attempts. Advance one delay at a time, using async variants when each attempt returns a promise.

Jitter complicates exact timing. Inject the random function or test within documented bounds. A seeded random source can help reproduction, but the scheduling test should not depend on an uncontrolled random delay.

8. Use Fake Timers With UI and Hook Tests

UI tests add rendering and effect scheduling around the timer. Advancing the clock may trigger a state update that the framework requires you to wrap in its update helper. Follow the UI testing library's current guidance and assert through user-visible output. Do not suppress update warnings, because they often mean the assertion ran before rendering settled.

A good UI timer test has four phases: render under fake timers, perform the user action, advance time inside the framework's update boundary, then query the visible result. If user-event tooling has its own timer integration option, configure it rather than mixing real waiting with a fake clock. The exact API belongs to that library and version, not Jest itself.

Fake timers can break tools that use timers internally for retry polling. A query that normally retries until an element appears may never progress unless the virtual clock advances. Prefer direct queries after intentionally flushing the application timer, or coordinate the library's async mechanism.

Browser end-to-end tests are different. Playwright and a real browser have their own page clocks and auto-waiting. Jest fake timers in the Node test process do not automatically control window.setTimeout inside the browser page. Use the framework's supported page clock controls or test the timer logic at a lower layer. Playwright auto-waiting guidance explains why adding sleeps in browser tests is not a substitute.

9. Configure Selective Faking and Timer Limits

jest.useFakeTimers() accepts configuration. doNotFake leaves selected APIs real when a library depends on them. For example, an environment with a custom performance implementation can preserve performance. Use selective faking only after identifying the dependency, because a partly real clock can create mixed-time behavior.

jest.useFakeTimers({
  doNotFake: ['performance'],
  now: new Date('2026-07-13T00:00:00.000Z'),
  timerLimit: 1_000,
});

The now option provides a starting virtual clock. timerLimit changes the maximum number of timers the fake-timer engine attempts for APIs that exhaust queues. Lowering it can make a runaway recursive test fail faster, but it should not make an infinite design acceptable.

Jest also offers legacy fake timers for compatibility. A new suite should use modern timers unless a verified dependency requires legacy behavior. Legacy and modern implementations expose different capabilities, and copying configuration between them causes confusing failures. Record a compatibility exception and plan its removal.

Do not fake every clock by default in a global setup file. Some tests validate real integration behavior, process signals, performance measurement, or third-party libraries. Scope the virtual environment to the smallest meaningful suite and provide shared lifecycle helpers if repetition becomes error-prone.

10. Jest fake timers qa: Debug Hangs and Leaks

A test that times out under fake timers often awaits work whose timer never advanced. Find the first unresolved promise, then identify whether its completion depends on setTimeout, an interval, a promise continuation, I/O, or another environment. Fake timers do not complete network requests or arbitrary promises.

Use this diagnostic sequence:

  1. Confirm useFakeTimers() ran before the subject scheduled work.
  2. Assert jest.getTimerCount() immediately after the action.
  3. Advance only to the next expected deadline.
  4. Switch to the async timer variant if callbacks schedule promises.
  5. Check for a recursive timer before using runAllTimers().
  6. Confirm the UI library's update boundary is respected.
  7. Restore real timers in cleanup even after failed assertions.

Spying on setTimeout can verify a delay contract, but modern fake timers do not automatically make every timer function a Jest mock for every matcher scenario. Follow the current documented pattern and use jest.spyOn(global, 'setTimeout') when call assertions are required, then restore the spy. Behavioral assertions are usually stronger than spying alone.

If tests leak timers, fail intentionally on a nonzero timer count in the focused suite and locate the owner. A pending retry or heartbeat can keep behavior alive after a test appears complete. Explicit cancellation improves production code as well as tests.

Design a complete time contract test

Before writing timer code, translate the requirement into observable checkpoints. For a session warning, checkpoints might be: no warning at 14 minutes 59 seconds, one warning at 15 minutes, expiration at 20 minutes, and no later callback after user activity resets the schedule. Each checkpoint should be advanced and asserted separately. This detects off-by-one errors that a single final runAllTimers() assertion would miss.

Separate scheduling from work where possible. A scheduler decides when to invoke a callback, while the callback performs I/O or state changes. Unit-test the delay calculation as a pure function, test scheduling with fake timers, and test the callback's I/O through ordinary async tests. This decomposition reduces the number of cases that need global clock replacement.

Define what happens during long clock jumps. For an interval, should advancing one hour produce sixty callbacks, one coalesced refresh, or immediate cancellation after a deadline? Fake timers will execute according to the implementation, but the product requirement must decide which result is correct. Write an explicit test for the chosen behavior, especially for background tabs, resumed devices, and job processors.

Also test rescheduling. User activity, configuration changes, and retry success often clear one timer and create another. Assert the pending count and ensure the old deadline cannot fire. A callback reference alone is not enough because a stale timer may invoke the same function with obsolete state.

Keep virtual-time tests independent from Jest's own per-test timeout. jest.setTimeout() changes how long the runner tolerates a real test execution. It does not advance application timers and is not a substitute for fake-time control. If a fake-timer test needs a much larger runner timeout, investigate unresolved I/O or promise ordering.

At suite level, track leaked timer incidents as design feedback. A component or service that cannot cancel its scheduler is difficult to shut down in production too. Expose a stop, dispose, or abort signal, verify it empties owned work, and call it in test cleanup. This turns timer determinism into a useful testability and lifecycle requirement.

Interview Questions and Answers

Q: Why use Jest fake timers instead of a short sleep?

Fake timers eliminate real waiting and scheduler races. They let me assert the state just before and at a deadline. A sleep is slower and can still fail under CI load.

Q: What is the difference between advanceTimersByTime and runAllTimers?

advanceTimersByTime moves a specific duration and preserves intermediate boundary assertions. runAllTimers exhausts a finite timer graph. I avoid runAllTimers for recursive scheduling and when exact delay is part of the requirement.

Q: When do you use an async timer API?

I use advanceTimersByTimeAsync or another async variant when due timer callbacks schedule promise work that affects the assertion. I schedule the result first, advance and await the virtual clock, then await the result.

Q: Does setSystemTime execute due timeouts?

No. It controls what system time reads return, but timer execution is advanced with timer APIs. I use it for expiry decisions and advance timers separately for callbacks.

Q: How do you test a recursive poller?

I advance one period or run only currently pending timers, assert each stage and stop condition, and verify cancellation removes the queue. I do not exhaust all timers because a correct poller may schedule indefinitely until stopped.

Q: What cleanup is required?

I clear or intentionally run owned pending timers, restore spies and mocks, then call useRealTimers. Cleanup runs in afterEach so a failed assertion does not contaminate later tests.

Q: When would you inject a clock instead?

I inject a clock when domain logic has substantial calendar or scheduling behavior. Unit tests can pass exact instants without global mutation, while a smaller integration layer verifies wiring with Jest fake timers.

Common Mistakes

  • Enabling fake timers after the subject already scheduled a real timeout.
  • Awaiting a timer-dependent promise before advancing virtual time.
  • Using synchronous advancement when callbacks create relevant promise work.
  • Calling runAllTimers() on an interval or recursive scheduler.
  • Assuming setSystemTime() also fires timer callbacks.
  • Forgetting to restore real timers when an assertion fails.
  • Mixing real sleeps, real clock reads, and fake deadlines in one test.
  • Advancing a UI timer outside the framework's required update boundary.
  • Using fake timers in Node and assuming they control timers inside a browser page.
  • Asserting only implementation calls instead of visible scheduling behavior.

Conclusion

Jest fake timers qa coverage is reliable when the test controls scheduling with precision. Install the virtual clock before the action, assert the pre-deadline state, advance with the narrowest synchronous or asynchronous API, verify results and cancellation, then restore the environment. Use setSystemTime for clock reads and timer advancement for callbacks.

Begin by replacing one real sleep in a debounce, retry, or expiration test. Prove the boundary on both sides and check the pending timer count. That single refactor usually produces a faster test and a clearer statement of the product's time contract.

Interview Questions and Answers

What do Jest fake timers provide?

They replace real timer and clock APIs with a virtual implementation that tests can control. This removes wall-clock waits and enables exact boundary assertions. They do not automatically resolve network I/O or every promise.

When would you use advanceTimersByTimeAsync?

I use it when a due timer callback schedules promise work that must complete before the assertion. I start the operation, await the async timer advancement, then await or assert the result.

How do setSystemTime and advanceTimersByTime differ?

`setSystemTime` changes what clock reads such as `Date.now()` observe. `advanceTimersByTime` moves virtual timer deadlines and executes due callbacks. A time-dependent test may need one or both for different reasons.

How do you test an interval safely?

I advance a bounded duration, assert each expected tick, cancel the interval, and verify no timers or later calls remain. I avoid `runAllTimers` because a live interval is intentionally unbounded.

What causes fake-timer test hangs?

Common causes are enabling fake timers too late, awaiting before advancing, using a synchronous method for promise-based callbacks, and relying on an unadvanced library retry timer. I inspect the pending count and trace the first unresolved dependency.

How do you isolate fake timer tests?

I scope installation to the relevant test or file and use `afterEach` to clear owned timers, restore mocks, and restore real timers. I avoid a global fake clock unless every test environment has been evaluated for compatibility.

Why inject a clock if Jest can fake Date?

An injected clock makes domain logic explicit and unit tests local, without mutating globals. Jest fake timers remain useful for verifying integration with actual scheduling APIs. The two techniques solve different layers of the design.

Frequently Asked Questions

How do I enable Jest fake timers?

Call `jest.useFakeTimers()` before the code under test schedules timers. In ESM tests, import `jest` from `@jest/globals`. Restore the environment with `jest.useRealTimers()` in reliable cleanup.

Why does my promise hang with Jest fake timers?

The promise probably depends on a timer that has not been advanced, or its timer callback schedules promise work. Start the operation without immediately awaiting it, use an async timer advancement method, then await the result. Check `jest.getTimerCount()` during diagnosis.

Should I use runAllTimers or advanceTimersByTime?

Use `advanceTimersByTime` when the duration or boundary is meaningful. Use `runAllTimers` only for a known finite timer graph where intermediate timing does not matter. Use pending or next-timer controls for recursive scheduling.

How do I mock the current date in Jest?

Enable fake timers and call `jest.setSystemTime()` with an explicit Date or timestamp. Include a time zone in string inputs. Advancing system time and firing scheduled callbacks are separate operations.

Can Jest fake timers test setInterval?

Yes. Advance by one or more intervals, assert the call count and behavior, then invoke cancellation. Avoid exhausting all timers because an active interval creates an unbounded schedule.

Do Jest fake timers control Playwright page timers?

No, not automatically. Jest controls the test environment where it is installed, while Playwright's page runs in a browser context. Use the browser framework's supported clock tooling or test the timing logic below the end-to-end layer.

What is jest.getTimerCount used for?

It returns the number of fake timers still scheduled. It is useful for asserting cancellation, finding leaks, and confirming that the subject actually scheduled work before time is advanced.

Related Guides