Concept · Testing practice

Test doubles: stubs, spies, mocks and fakes

The five kinds of stand-in for a real dependency in tests, what each is for, and why outcome-based fakes usually beat interaction mocks.

3 minmockstubfakespy

A test double stands in for a real dependency — a database, a payment API, the clock — so a unit test can run fast and deterministically. Gerard Meszaros named five kinds, and the names are worth using precisely:

DoubleWhat it doesExample
DummyFills a parameter that's never usednil logger, empty struct
StubReturns canned answersa clock that always returns 09:00; a store that always returns "not found"
SpyA stub that also records callsa mailer that appends each sent message to a slice
MockPre-programmed with expected calls; fails the test if they don't happen"expect Charge(order 42, 15000) exactly once"
FakeA working, simplified implementationan in-memory repository backed by a map

Writing them in Go

With small, consumer-defined interfaces, hand-written doubles take a few lines:

go
type fakeOrders struct{ saved map[string]Order }

func (f *fakeOrders) Save(_ context.Context, o Order) error {
	f.saved[o.ID] = o
	return nil
}

type stubClock struct{ now time.Time }

func (c stubClock) Now() time.Time { return c.now }

Mocking libraries such as gomock or testify/mock generate mocks from interfaces; in TypeScript, Vitest and Jest provide vi.fn() / jest.fn() spies and module mocking.

Outcomes over interactions

Two styles of assertion:

  • State/outcome-based: after the action, check results — "the order is saved with status Paid", "one receipt was sent". Uses fakes, stubs and spies.
  • Interaction-based: check that specific calls happened in a specific way — "Save was called, then Publish, with these arguments". Uses mocks.

Interaction tests couple to how the code works, so they break on harmless refactors, and they can pass while the real behaviour is wrong. Prefer outcome-based tests, and reserve mocks for interactions that are the behaviour — "the payment provider must be called exactly once", "no email is sent for opted-out users".

Guidelines

  • Double what's slow, non-deterministic or external; use the real thing for pure in-process logic.
  • Don't mock types you don't own — wrap a vendor SDK in your own adapter and double that.
  • Keep fakes honest by running a shared contract test against both the fake and the real implementation (Liskov substitution in practice).
  • Prove the real wiring with a few integration tests; see the test pyramid.

Learn it in depth