Tags: web-dev concept

Test Doubles

Date: 2026-08-17


Stand-ins for real dependencies during a test. There are five distinct kinds, everyone calls all of them “mocks”, and the confusion matters because only one of them asserts on interactions — which is the one that makes tests brittle.


A test double replaces a real dependency in a test. “Mock” is the colloquial term for all of them and the precise term for one.

The five

KindWhat it does
DummyPassed to satisfy a signature, never used
StubReturns canned answers. No assertions
SpyA real thing, wrapped to record calls
MockA stub that asserts on how it was called
FakeA working lightweight implementation
// STUB — provides a value
vi.mocked(getPrice).mockReturnValue(4999)
 
// SPY — records, real behaviour intact
const spy = vi.spyOn(analytics, 'track')
 
// MOCK — asserts on the interaction
expect(sendEmail).toHaveBeenCalledWith(
  'a@example.com', 'Order confirmed')
 
// FAKE — a real implementation, simplified
const repo = new InMemoryOrderRepository()

The distinction that matters

Stubs answer questions. Mocks make demands.

// STUB — asserts on the OUTCOME
getPrice.mockReturnValue(4999)
expect(calculateTotal(basket)).toBe(5999)
  ← survives refactoring
 
// MOCK — asserts on the MECHANISM
expect(getPrice).toHaveBeenCalledTimes(1)
expect(getPrice).toHaveBeenCalledWith('SKU-1')
  ← breaks if you cache, batch, or
    reorder the calls

Mock assertions couple the test to the implementation. Change how the code achieves the result and the test fails despite the behaviour being identical — which teaches people that refactoring is expensive — Unit Testing.

Prefer stubs. Reach for mocks only when the interaction is the behaviour — that an email was sent, that a payment was captured, that an event was recorded. In those cases the call is the outcome.

Fakes are underused

class InMemoryOrderRepository {
  #orders = new Map()
  async save(o) { this.#orders.set(o.id, o) }
  async find(id) { return this.#orders.get(id) ?? null }
}

A fake behaves correctly, so tests read naturally and don’t need re-stubbing per case. Worth building for anything used across many tests — a repository, a clock, a feature flag provider.

The risk is divergence. A fake that drifts from the real implementation gives confidence in behaviour that no longer exists — so run the same contract tests against both where you can.

Where to draw the line

DOUBLE THESE
  third-party HTTP APIs
  payment providers
  email and SMS senders
  time and randomness
  anything slow, paid, or with
    side effects on the world

DON'T DOUBLE THESE
  your own modules
    ← doubling them means the unit
      boundary is wrong
  your database
    ← use a real one — Integration Testing
  the framework

See: Integration Testing

Mocking your own code is the strongest smell in this note. If a function needs four of your own modules doubled, the test is documenting entanglement rather than verifying behaviour — Coupling and Cohesion.

Time and randomness

The two doubles nearly every codebase needs and few have:

vi.setSystemTime(new Date('2026-08-17T10:00:00Z'))

Untested time handling is where timezone and boundary bugs live — an order placed at 23:58 on the last day of the month, a subscription renewing across a daylight-saving change. Freezing the clock makes those testable at all.

Inject them rather than reaching for globals, so they’re substitutable without a mocking framework:

function createOrder(items, { now = () => new Date() } = {}) { }

Contract drift

The failure that makes heavy doubling dangerous:

your stub returns    { total: 4999 }
the API now returns  { totalPence: 4999 }

every test passes
production is broken

Doubles freeze your beliefs about a dependency at the moment you wrote them. Guard against it with contract tests against the real thing, running less often — nightly rather than per commit — or by intercepting at the network layer with recorded real responses.

This is the strongest argument for weighting towards integration tests, which use the real collaborators and can’t drift — Testing Strategy.