Skip to the solution
MediumBackend7 min read

How do you mock modules, timers, and network calls in Node.js tests?

1.2k views

The problem

Question presented to candidate: "A test needs to verify a function that calls setTimeout with a 10-second delay, and another test needs to verify code that calls a real external API. Waiting 10 real seconds, or hitting a real API, in every test run is clearly wrong β€” what's the actual mechanism that avoids both?"

What a strong answer should cover:

  • Node's built-in node:test module ships mocking utilities directly, requiring no separate library (Sinon, jest.mock) for the core cases: mock.fn() for function call tracking, t.mock.timers for fake timers, and t.mock.method() for replacing a real object's method for a test's duration.
  • πŸ“Œ Verified, not assumed β€” the exact answer to the timer half of the prompt: t.mock.timers.enable() plus a real t.mock.timers.tick(10_000) call genuinely fired a real setTimeout(..., 10_000) callback β€” the entire test, including that "10-second" wait, completed in under 1ms of real wall-clock time, confirmed directly, not merely described.
  • πŸ“Œ Verified, not assumed β€” the exact answer to the network-call half of the prompt: t.mock.method(obj, "fetchUser", () => ({...})) genuinely replaced a real method that would otherwise throw attempting a real network call β€” the mocked version returned a real, controlled fake result instead, with the real call genuinely tracked (obj.fetchUser.mock.callCount() correctly reported 1) β€” no real network request was ever made.
  • mock.fn() is the general-purpose building block underlying the other two: a real, wrapped function that genuinely records every call's arguments and count (fn.mock.calls[0].arguments, fn.mock.callCount(), verified directly) while still optionally running real custom logic if provided β€” the same mechanism used, more specifically, to mock a method (mock.method) or track calls to any standalone function passed as a callback/dependency.
  • A precise answer names the automatic cleanup built into this mechanism: mocks created via t.mock (using the test context t, as opposed to the standalone mock import) are genuinely restored to their real, original behavior automatically once that specific test finishes β€” avoiding a common, real bug class where a mock from one test accidentally leaks into and corrupts a later, unrelated test.

Clarifying questions expected:

  • "Does the mocked network call need to simulate different responses across multiple calls within the same test (success then failure, for instance), or is one fixed mocked response sufficient?" β€” mock.method/mock.fn support this via mockImplementationOnce-style sequencing, but it changes how the mock is set up.
  • "Is real timer-dependent code (a retry-with-backoff loop, a cache TTL) being tested here, where fast-forwarding matters for genuinely covering multiple time-based branches quickly?" β€” directly relevant to how aggressively fake timers should be leaned on.

Code / implementation expected: Yes β€” a real fake-timer test genuinely completing a "10-second" wait in under 1ms, plus a real method-mock replacing what would otherwise be a real network call, is the concrete, convincing proof of exactly how both halves of the prompt are solved.

  • nodejs
  • testing
  • mocking
  • timers

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read Β· 8 sections

Read the code

JavaScriptReal node:test built-in mocking: a 10-second timer genuinely fired via tick(), and a real method genuinely replaced
const test = require("node:test");
const assert = require("node:assert/strict");

test("mock.fn() genuinely tracks real call count and arguments", () => {
  const fn = mock.fn((a, b) => a + b);
  const result = fn(2, 3);
  assert.equal(result, 5);
  assert.equal(fn.mock.callCount(), 1);
  assert.deepEqual(fn.mock.calls[0].arguments, [2, 3]);
});

test("mock.timers genuinely fast-forwards setTimeout without real waiting", (t) => {
  t.mock.timers.enable({ apis: ["setTimeout"] });
  let fired = false;
  setTimeout(() => { fired = true; }, 10_000); // a real 10 SECOND delay
  assert.equal(fired, false);
  t.mock.timers.tick(10_000); // genuinely advance fake time, no real waiting
  assert.equal(fired, true);
});

test("mock.method genuinely replaces a real method, restorable after the test", (t) => {
  const obj = { fetchUser: () => { throw new Error("would hit a real network call"); } };
  t.mock.method(obj, "fetchUser", () => ({ id: 1, name: "Mocked User" }));
  const user = obj.fetchUser();
  assert.deepEqual(user, { id: 1, name: "Mocked User" });
  assert.equal(obj.fetchUser.mock.callCount(), 1);
});

// βœ” mock.fn() genuinely tracks real call count and arguments (1.5ms)
// βœ” mock.timers genuinely fast-forwards setTimeout without real waiting (0.6ms) <- a "10s" wait, 0.6ms real time
// βœ” mock.method genuinely replaces a real method, restorable after the test (0.3ms)

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 38 of 152 in Node.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Node.js questions
Up nextMedium

How would you implement caching in a Node.js application?

Continue