Mock in JavaScript: swapping a dependency out for the length of a test

A mock replaces a real dependency with a controlled double during a test: the four families of doubles, vi.fn, vi.mock and the simulated clock.
3 min read
Believemy logo

Testing a function that sends an email raises an obvious problem: nobody wants a thousand messages genuinely leaving the building every time the suite runs.

The answer fits in one sentence: the sending is replaced by a double, and the test checks the double was asked to work.


Definition

A mock is a fake implementation standing in for a real dependency during a test. It hands back an answer decided in advance and records everything asked of it: how many calls, with which arguments, in what order.

JAVASCRIPT
import { expect, it, vi } from "vitest";
import { notify } from "./notifications.js";

it("sends one message per recipient", () => {
  const send = vi.fn();

  notify(["lea@example.com"], send);

  expect(send).toHaveBeenCalledTimes(1);
  expect(send).toHaveBeenCalledWith("lea@example.com");
});

The vi.fn() function does nothing and returns undefined, which is enough here. Its value lies elsewhere: it keeps a record of its calls, and that record is what the test questions.


Four doubles, routinely confused

NameWhat it does
SpyWatches the calls without changing the real behavior
StubHands back a fixed answer, with no logic at all
MockA stub whose calls are afterwards verified
FakeA simplified but genuinely working implementation

Everyday vocabulary calls all of this a "mock", and the shortcut carries little consequence. The distinction worth keeping is the last one: a fake, such as an in-memory database, actually works, where the other three merely answer.


Replacing a module, stopping time

Passing the dependency in as an argument is the simplest route, but it is not always available. Vitest can also intercept a whole import, and replace the engine's clock.

JAVASCRIPT
vi.mock("./mailer.js", () => ({
  send: vi.fn().mockResolvedValue({ id: "msg_1" }),
}));

vi.useFakeTimers();
retryInOneHour();
vi.advanceTimersByTime(3600 * 1000);
expect(send).toHaveBeenCalledTimes(1);
Good to know

The simulated clock is the cure for slow tests: without it, checking a retry scheduled with setTimeout() would mean genuinely waiting an hour.


Frequently asked questions

Question

Should every dependency be mocked?

No, only those that leave the program: network, disk, clock, randomness, payment. A calculation function imported by another has no reason to be replaced, and replacing it only blinds the test to any incompatibility between the two.


Question

What happens when the real dependency changes?

That is the structural weakness of the technique: the double keeps answering as before, the suite stays green, and the breakage only surfaces in production. The usual safeguard is a handful of integration tests that do call the real implementation.


Question

How is a network call simulated?

Two schools. The quickest replaces fetch() with a double returning a ready-made response. The more faithful one intercepts the request at the simulated network level, which lets the real code of Axios or of whatever client is in use run. The first is enough for a unit test, the second is better as soon as headers or an error code matter.

Related terms

Discover our javaScript glossary

Every word of JavaScript explained simply: keywords, built-in objects, methods, errors and concepts. Clear definitions and examples that actually run, to learn and to troubleshoot.

Share this article

Want to help us? Share this article on your networks or even better: on your site, in an article or in your newsletter.