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.
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:
| Double | What it does | Example |
|---|---|---|
| Dummy | Fills a parameter that's never used | nil logger, empty struct |
| Stub | Returns canned answers | a clock that always returns 09:00; a store that always returns "not found" |
| Spy | A stub that also records calls | a mailer that appends each sent message to a slice |
| Mock | Pre-programmed with expected calls; fails the test if they don't happen | "expect Charge(order 42, 15000) exactly once" |
| Fake | A working, simplified implementation | an in-memory repository backed by a map |
Writing them in Go
With small, consumer-defined interfaces, hand-written doubles take a few lines:
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 — "
Savewas called, thenPublish, 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.