Intermediate7 min read

Mocking & Test Doubles

Stub, spy, mock, fake — five different tools that get lumped under one word. Know which to reach for.

"Mock" is overloaded. Gerard Meszaros split test doubles into precise categories, and using the right one keeps tests both fast and honest. The point of a double is to replace a real collaborator that is slow, non-deterministic, or has side effects.

The five doubles

  • Dummy — passed around but never used; fills a parameter list.
  • Stub — returns canned answers to calls made during the test.
  • Spy — a stub that also records how it was called, for later assertions.
  • Mock — pre-programmed with expectations; fails the test if they are not met.
  • Fake — a working but lightweight implementation, e.g. an in-memory repository.
import { vi, describe, it, expect } from 'vitest';

// Stub: canned return value
const clock = { now: vi.fn().mockReturnValue(0) };

// Spy: assert on the interaction
const logger = { warn: vi.fn() };
applyDiscount(cart, logger);
expect(logger.warn).toHaveBeenCalledWith('expired coupon');

State vs interaction testing

Prefer asserting on the resulting state (what the system became) over the interaction (which methods were called). Interaction assertions couple the test to the implementation, so they break during refactors. Reach for spies and mocks when the interaction is the behaviour — sending an email, charging a card.

Tip: If a test needs five mocks to run, that is a design smell, not a mocking problem. The unit under test probably has too many collaborators.

Mock at the edges

Mock the boundaries you do not own — the network, the filesystem, the clock — and let your own code run for real. A test that mocks the very thing it is supposed to verify proves nothing. For HTTP specifically, intercept at the network layer so your real client code still executes.

See request/response doubles in action in the API Testing Sandbox.

Practice this

Read next