/ #node:test #testing #node 

Mocking/stubbing the Date and timers (setTimeout) in Node.js tests with built-in `node:test` MockTimers

MockTimers is an experimental class in the node:test module which concerns itself with allowing mocking of timers (setTimeout, setInterval, setImmediate) and the Date built-ins.

Table of Contents

The MockTimers API is available as part the the node:test built-in module since Node 20.4.0 (for mocking timers only), and the API was changed to allow for mocking of the date in Node v21.2.0.

In this post we’ll look at mocking the Date using mock.timers.enable({ apis: ['Date']}) and then how to mock setTimeout using the Node 20 syntax, mock.timers.enable(['setTimeout']).

To skip to the full examples use the following links:

Date mocking

Warning: The API demonstrated here is only available in Node 21.2.0+ and is still marked as experimental, so may change in Node.js versions without a major version bump. See stability Node.js docs.

The following code is available at node-test-runner-examples/src/02.03-date-mock-timers.test.js.

We can use mock.timers.enable({ apis: ['Date'], now: new Date() }) from the node:test module to set a specific date in tests via the now parameter. We can use mock.timers.reset() to unmock the API.

A simple example of this is as follows:

import { test } from 'node:test';
import assert from 'node:assert/strict';

test('mock.timers can mock Date and be reset', (t) => {
  t.mock.timers.enable({
    apis: ['Date'],
    now: new Date('2023-05-14T11:01:58.135Z'),
  });

  assert.deepEqual(new Date(), new Date('2023-05-14T11:01:58.135Z'));

  t.mock.timers.reset();
  assert(new Date().getFullYear() > 2023);
});

mock.timers is available both from the “test context” as above (the parameter passed to the test implementation function) as t.mock.timers but also from the node:test mock variable. We can therefore reset the Date mocking outside of the test context, for example in an afterEach hook.

mock.timers.enable also affects the output of Date.now(), and we can control the returned data after the mock.timers.enable call with mock.timers.tick(milliseconds) or mock.timers.setTime(milliseconds).

We demonstrate these in the following code snippet:

import { test, mock, afterEach } from 'node:test';
// no changes to other imports
afterEach(() => {
  mock.timers.reset();
});
// no change to the other test
test('mock.timers can mock Date.now() and be ticked forward', (t) => {
  t.mock.timers.enable({
    apis: ['Date'],
    now: new Date('2023-05-14T11:01:58.135Z'),
  });

  assert.deepEqual(Date.now(), new Date('2023-05-14T11:01:58.135Z').valueOf());

  // advance by 24h
  t.mock.timers.tick(1000 * 60 * 60 * 24);

  assert.deepEqual(Date.now(), new Date('2023-05-15T11:01:58.135Z').valueOf());

  t.mock.timers.setTime(20000);
  assert.deepEqual(Date.now(), 20000);
});

Running the above tests with node will yield the following output (since we’re using node:test in our module, our file is will run as “Node Test Runner” and output accordingly):

node src/02.03-date-mock-timers.test.js
✔ mock.timers can mock Date and be reset (1.870625ms)
✔ mock.timers can mock Date.now() and be ticked forward (0.189166ms)
(node:2751) ExperimentalWarning: The MockTimers API is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 7.150875

We’ve now seen how to use the Node 21+ MockTimers API to mock new Date() and Date.now(), next we’ll see how to use the Node 20+ MockTimers API to mock timers like setTimeout.

Timer mocking setTimeout, setInterval

Warning: The API demonstrated here is only available in Node 20.4.0+ and is still marked as experimental, so may change in Node.js versions without a major version bump. See stability Node.js docs.

The following code is available at node-test-runner-examples/src/02.03-timeout-mock-timers.test.js.

setTimeout, setInterval and setImmediate can be mocked in Node >20.2.0 by using mock.timers.enable(['setTimeout']) followed by mock.timers.tick(milliseconds) or mock.timers.runAll().

As demonstrated by the following code, we’ll have 2 “userland-promisified” timers with different delays, on resolution of each we will call a node:test Mock function.

We’ll show how we can use mock.timers.tick to advance the timers to the point where one of them resolves and how we can use mock.timers.runAll to run all pending timers.

import { test, mock } from 'node:test';
import assert from 'node:assert/strict';

const wait = (ms) => new Promise((res) => setTimeout(res, ms));

test('mock.timers can mock setTimeout', async (t) => {
  t.mock.timers.enable(['setTimeout']);

  const fn = mock.fn();

  const wait1 = wait(1000).then(() => fn());
  const wait2 = wait(10000).then(() => fn());

  assert.equal(fn.mock.callCount(), 0, 'fn should not have been called');

  t.mock.timers.tick(1000);
  await wait1; // await is required to flush promises

  assert.equal(fn.mock.callCount(), 1, 'fn should have been called once');

  t.mock.timers.runAll();
  await wait2; // await is required to flush promises

  assert.equal(fn.mock.callCount(), 2, 'fn should have been called twice');
});

Running the above tests with node will yield the following output (since we’re using node:test in our module, our file is will run as “Node Test Runner” and output accordingly):

node src/02.03-timeout-mock-timers.test.js
✔ mock.timers can mock setTimeout (2.241833ms)
(node:1878) ExperimentalWarning: The MockTimers API is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 9.940875

We’ve now seen how to use the Node 20+ MockTimers API to mock timers like setTimeout.

Author

Hugo Di Francesco

Co-author of "Professional JavaScript", "Front-End Development Projects with Vue.js" with Packt, "The Jest Handbook" (self-published). Hugo runs the Code with Hugo website helping over 100,000 developers every month and holds an MEng in Mathematical Computation from University College London (UCL). He has used JavaScript extensively to create scalable and performant platforms at companies such as Canon, Elsevier and (currently) Eurostar.

Get The Jest Handbook (100 pages)

Take your JavaScript testing to the next level by learning the ins and outs of Jest, the top JavaScript testing library.