Fake timers and fake system time

Sometimes you will need to test some functionality which uses setTimeout(), which can be several or many seconds

Instead of actually waiting those seconds, in Vitest and Jest we can use fake timers.

They behave exactly like regular setTimeout() calls, but you can decide when to run them or advance the time by a number of milliseconds.

To use mock setTimeout(), setInterval() and some Date() functions, start by calling vi.useFakeTimers() (or jest.useFakeTimers() - everything in the lesson is the same for Vitest as it is for Jest).

You will normally see this at the top of a test file, or in beforeAll()

This replaces the default setTimeout with one that is managed by your test runner.

Important notes about running fake timers on HowToTestFrontend.com

  • Due to browser limitations, we can't mock timers like you can when you run tests on the command line. We still describe how they work and the examples below (with a note saying you can't run it in our test runner)
  • Examples below use Vitest (vi.*). In Jest, use the equivalent jest.* APIs.

Simple example

Here is a simple example:

beforeEach(() => {
  vi.useFakeTimers();
  // or jest.useFakeTimers();
});

it('should test runAllTimers', () => {
  let timeoutHasRun = false;

  setTimeout(() => {
    timeoutHasRun = true;
  }, 10_000);

  expect(timeoutHasRun).toBe(false);

  vi.runAllTimers(); // in jest: jest.runAllTimers()

  expect(timeoutHasRun).toBe(true);
});

When you run this, it doesn't actually take 10 seconds before the setTimeout callback has run. It will be run immediately after vi.runAllTimers()

Use real timers again

Sometimes you might want to use a mix of fake timers and real timers.

Often vi.useRealTimers() (or jest.useRealTimers()) would be added at the end of a test (or afterEach/afterAll).

Move forward in time

There are several ways to move forward in time (and run any timers/intervals which should have run)

advanceTimersByTime()

function delayedGreeting(
  name,
  callback
) {
  setTimeout(() => {
    callback(`Hello, ${name}!`);
  }, 1000);
}

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

test('delayed greeting works with fake timers', () => {
  const mockCallback = vi.fn(); // or jest.fn()

  delayedGreeting(
    'Alice',
    mockCallback
  );

  expect(
    mockCallback
  ).not.toHaveBeenCalled();

  vi.advanceTimersByTime(1000); // or jest.advanceTimersByTime(1000)

  expect(
    mockCallback
  ).toHaveBeenCalledWith(
    'Hello, Alice!'
  );
});

Note: advanceTimersByTime won't work in our test runner in the browser. It works in Jest and Vitest though!

runAllTimers()

This will run all timers which haven't run yet.

test('delayed greeting works with fake timers', () => {
  const mockCallback = vi.fn(); // or jest.fn()

  delayedGreeting(
    'Alice',
    mockCallback
  );

  expect(
    mockCallback
  ).not.toHaveBeenCalled();

  vi.runAllTimers(); // or jest.runAllTimers()

  expect(
    mockCallback
  ).toHaveBeenCalledWith(
    'Hello, Alice!'
  );
});

runOnlyPendingTimers()

This runs only the timers that are currently pending - that means the timers that have been scheduled but not yet executed.

Unlike runAllTimers(), this won't run any new timers that get created during the execution of pending timers.

This is useful when you want more control over which timers execute, especially when dealing with recursive timers or timers that schedule other timers.

For example, if a timer callback creates another timer, runOnlyPendingTimers() will only run the original timer, not the newly created one.

test('delayed greeting works with fake timers', () => {
  const mockCallback = vi.fn(); // or jest.fn()

  delayedGreeting(
    'Alice',
    mockCallback
  );

  expect(
    mockCallback
  ).not.toHaveBeenCalled();

  vi.runOnlyPendingTimers(); // or jest.runOnlyPendingTimers()

  expect(
    mockCallback
  ).toHaveBeenCalledWith(
    'Hello, Alice!'
  );
});

Testing setInterval()

Testing and mocking setInterval works in a very similar way!

function createCounter() {
  let count = 0;
  const interval = setInterval(() => {
    count++;
    console.log(`Count: ${count}`);
  }, 100);

  return {
    getCount: () => count,
    stop: () => clearInterval(interval),
  };
}

test('counter increments every 100ms', () => {
  const counter = createCounter();

  expect(counter.getCount()).toBe(0);

  vi.advanceTimersByTime(100); // or jest.advanceTimersByTime(100)
  expect(counter.getCount()).toBe(1);

  vi.advanceTimersByTime(200);
  expect(counter.getCount()).toBe(3);

  counter.stop();
});

(Note: advanceTimersByTime() is not implemented in our test runner, but works without problem on a real jest or vitest runner!)

Setting fake dates

You can use setSystemTime to set the internal date.

function getTimestamp() {
  return Date.now();
}

test('can control current time', () => {
  const fixedTime = new Date(
    '2000-01-01T00:00:00Z'
  );
  vi.setSystemTime(fixedTime); // or jest.setSystemTime(...)

  expect(getTimestamp()).toBe(
    fixedTime.getTime()
  ); // JS date is now showing as year 2000

  // you can combine this with things that advance the time:
  vi.advanceTimersByTime(5000);
  expect(getTimestamp()).toBe(
    fixedTime.getTime() + 5000
  );
});

Issues you may face

When you start using useFakeTimers, other areas of your code may work.

This is especially true for some libraries that do data fetching (which internally sometimes end up using setTimeout()), or other async behaviour (which again somewhere ends up using setTimeout).

In those cases they will often break, without careful (and often complex) use of runPendingTimers().

Lesson Task

There are a few steps to using fake timers in Jest or Vitest, so this lesson has a few things that you have to do.

First you need to call vi.useFakeTimers(). You might want to call vi.useRealTimers() after your tests to restore it, although in this example test you won't have to do this.

Then you need to run only pending timers...

After doing that, the timer (scheduled for 10 seconds) will run, and the final assertion will pass.

Ready to try running tests for this lesson?