Prefer Fakes Over Mocks
When a test needs a stand-in for a collaborator (a database, clock, queue, HTTP
client), reach for a fake before a mock in almost every case. A fake tests
what the code does (observable state and behavior); an interaction mock tests
how the code does it (the exact calls made) — and "how" is the part you most
want to be free to change. This complements [[test-design]] (which covers test
structure, and deliberately defers fakes-vs-mocks here) and pairs with
[[sim-zero-mode]] for deterministic test environments.
Principles are language-agnostic; per-language bindings are at the end.
The taxonomy (so the terms are precise)
- Dummy — passed but never used (fills a parameter).
- Stub — returns canned answers to the calls a test makes; no logic.
- Fake — a real, lightweight working implementation: an in-memory
repository, a fake clock, an in-memory message bus. Behaves like the real
thing within the test.
- Mock — pre-programmed with expectations and verifies the interactions
(which methods were called, with what, in what order).
Grounded in Martin Fowler, Mocks Aren't Stubs / Fake, Don't Mock, and
Software Engineering at Google (Test Doubles).
Rules
Default to a fake. A fake is a working implementation that behaves like
the real collaborator (in-memory store, fake clock, in-memory queue). Tests
built on fakes assert the outcome — the resulting state — so they read
like a spec and survive refactors that preserve behavior.
Mocks couple tests to implementation. A test that asserts
verify(repo).save(x) or mock.assert_called_with(...) passes or fails based
on the call sequence, not the result. It breaks when you refactor internals
that still produce the right outcome, and — worse — it can stay green while
the real integration is broken, because nothing exercised a real
implementation. Brittle and low-signal.
A fake must stay faithful, or it lies. The risk with fakes is drift: the
in-memory version diverges from production behavior. Counter it: the fake is
owned and maintained by the team that owns the real component, and it runs
against the same contract tests as the real implementation (one shared
test suite, two implementations). A fake without contract tests is a
liability, not an asset.
Prefer state verification over interaction verification. Assert the
observable result after the act (assert repo.get(id) == updated), not the
calls made to get there. Verify interactions only when the interaction is
the externally-observable behavior.
When a mock or stub is genuinely justified (keep it narrow):
- The real collaborator is unmockable or expensive and no fake exists yet —
a stub returning a canned value is the pragmatic bridge (and a fake is the
follow-up).
- You must inject a failure (timeout, 500, disk-full) you cannot otherwise
trigger — stub the error path.
- The interaction itself is the contract — e.g. "publishes exactly one
OrderPlaced event", "must not call the payment API twice". Then verify
that one interaction, deliberately, not the whole call graph.
Don't mock types you don't own. Mocking a third-party SDK couples your
tests to its surface, which can change or which you may misunderstand. Wrap
it behind your own narrow interface and fake that interface instead.
Detecting the smell (review / lint)
- A test file dominated by
when(...).thenReturn(...) + verify(...) /
mock.assert_called* / expect(fn).toHaveBeenCalledWith(...) is testing
interactions — ask what state it should assert instead.
- A growing pile of mock setup at the top of every test = a missing fake.
Build the fake once; delete the boilerplate everywhere.
- Mocks of types from outside the codebase = a missing wrapper interface.
Honest cost
A good fake costs real effort up front — you write a working implementation and
keep it contract-tested. That cost is amortized across every test that uses
it and pays back in tests that don't break on refactors. For a genuine one-off
boundary you can't fake yet, a stub is the right, cheaper call — just don't let
one-offs become the house style.
When to use / not
- Use when choosing a test double, or reviewing tests heavy in mock setup
and interaction assertions.
- Not for test structure/naming (→ [[test-design]]) or for standing up a
whole deterministic test environment (→ [[sim-zero-mode]]).
Per-language bindings
- Python: prefer an in-memory fake class (e.g.
InMemoryUserRepo
implementing the same protocol) over unittest.mock / MagicMock; reserve
mock/monkeypatch for error injection and un-fakeable boundaries; assert on
the fake's resulting state, not assert_called_with.
- Java/JVM: a hand-written or shared
FakeXxx implementing the interface
over Mockito verify(...); if you must use Mockito, prefer stubbed returns +
state assertions over verify interaction checks; run the fake against the
interface's contract tests.
- TypeScript: an in-memory object implementing the interface over
jest.fn()
/ vi.fn() spies; keep toHaveBeenCalledWith for the rare interaction-as-
contract case.
- Go: a small fake struct satisfying the interface over
gomock; idiomatic
Go already favors interface + fake; reserve generated mocks for interaction
contracts.
1---2name: prefer-fakes-over-mocks3description: Use when choosing or reviewing test doubles — deciding between a fake, stub, or mock for a collaborator. Default to fakes (working in-memory implementations); reserve interaction mocks for the narrow cases where the interaction itself is the contract.4---56# Prefer Fakes Over Mocks78When a test needs a stand-in for a collaborator (a database, clock, queue, HTTP9client), **reach for a fake before a mock in almost every case.** A fake tests10*what the code does* (observable state and behavior); an interaction mock tests11*how the code does it* (the exact calls made) — and "how" is the part you most12want to be free to change. This complements [[test-design]] (which covers test13*structure*, and deliberately defers fakes-vs-mocks here) and pairs with14[[sim-zero-mode]] for deterministic test environments.1516Principles are **language-agnostic**; per-language bindings are at the end.1718## The taxonomy (so the terms are precise)1920- **Dummy** — passed but never used (fills a parameter).21- **Stub** — returns canned answers to the calls a test makes; no logic.22- **Fake** — a real, lightweight *working* implementation: an in-memory23 repository, a fake clock, an in-memory message bus. Behaves like the real24 thing within the test.25- **Mock** — pre-programmed with expectations and **verifies the interactions**26 (which methods were called, with what, in what order).2728Grounded in Martin Fowler, *Mocks Aren't Stubs* / *Fake, Don't Mock*, and29*Software Engineering at Google* (Test Doubles).3031## Rules32331. **Default to a fake.** A fake is a working implementation that behaves like34 the real collaborator (in-memory store, fake clock, in-memory queue). Tests35 built on fakes assert the **outcome** — the resulting state — so they read36 like a spec and survive refactors that preserve behavior.37382. **Mocks couple tests to implementation.** A test that asserts39 `verify(repo).save(x)` or `mock.assert_called_with(...)` passes or fails based40 on the *call sequence*, not the result. It breaks when you refactor internals41 that still produce the right outcome, and — worse — it can stay **green while42 the real integration is broken**, because nothing exercised a real43 implementation. Brittle and low-signal.44453. **A fake must stay faithful, or it lies.** The risk with fakes is drift: the46 in-memory version diverges from production behavior. Counter it: the fake is47 **owned and maintained by the team that owns the real component**, and it runs48 against the **same contract tests** as the real implementation (one shared49 test suite, two implementations). A fake without contract tests is a50 liability, not an asset.51524. **Prefer state verification over interaction verification.** Assert the53 observable result after the act (`assert repo.get(id) == updated`), not the54 calls made to get there. Verify interactions only when the interaction *is*55 the externally-observable behavior.56575. **When a mock or stub is genuinely justified (keep it narrow):**58 - The real collaborator is unmockable or expensive and **no fake exists yet** —59 a stub returning a canned value is the pragmatic bridge (and a fake is the60 follow-up).61 - You must **inject a failure** (timeout, 500, disk-full) you cannot otherwise62 trigger — stub the error path.63 - The **interaction itself is the contract** — e.g. "publishes exactly one64 `OrderPlaced` event", "must not call the payment API twice". Then verify65 that *one* interaction, deliberately, not the whole call graph.66676. **Don't mock types you don't own.** Mocking a third-party SDK couples your68 tests to *its* surface, which can change or which you may misunderstand. Wrap69 it behind your own narrow interface and **fake that interface** instead.7071## Detecting the smell (review / lint)7273- A test file dominated by `when(...).thenReturn(...)` + `verify(...)` /74 `mock.assert_called*` / `expect(fn).toHaveBeenCalledWith(...)` is testing75 *interactions* — ask what **state** it should assert instead.76- A growing pile of mock setup at the top of every test = **a missing fake**.77 Build the fake once; delete the boilerplate everywhere.78- Mocks of types from outside the codebase = a **missing wrapper interface**.7980## Honest cost8182A good fake costs real effort up front — you write a working implementation and83keep it contract-tested. That cost is **amortized** across every test that uses84it and pays back in tests that don't break on refactors. For a genuine one-off85boundary you can't fake yet, a stub is the right, cheaper call — just don't let86one-offs become the house style.8788## When to use / not8990- **Use** when choosing a test double, or reviewing tests heavy in mock setup91 and interaction assertions.92- **Not** for test *structure/naming* (→ [[test-design]]) or for standing up a93 whole deterministic test environment (→ [[sim-zero-mode]]).9495## Per-language bindings9697- **Python:** prefer an in-memory fake class (e.g. `InMemoryUserRepo`98 implementing the same protocol) over `unittest.mock` / `MagicMock`; reserve99 `mock`/`monkeypatch` for error injection and un-fakeable boundaries; assert on100 the fake's resulting state, not `assert_called_with`.101- **Java/JVM:** a hand-written or shared `FakeXxx` implementing the interface102 over Mockito `verify(...)`; if you must use Mockito, prefer stubbed returns +103 state assertions over `verify` interaction checks; run the fake against the104 interface's contract tests.105- **TypeScript:** an in-memory object implementing the interface over `jest.fn()`106 / `vi.fn()` spies; keep `toHaveBeenCalledWith` for the rare interaction-as-107 contract case.108- **Go:** a small fake struct satisfying the interface over `gomock`; idiomatic109 Go already favors interface + fake; reserve generated mocks for interaction110 contracts.