tech, developers, and the code underneath

issue 234· essay·

The test double taxonomy

Mock, stub, fake, spy, dummy. The words are used interchangeably and the distinctions are the useful part.

Almost everyone says "mock" for every kind of test double. The categories are worth keeping distinct, because choosing the wrong one is how test suites become brittle.

the five#

Dummy — passed to satisfy a signature, never used.

python
service = OrderService(logger=None, clock=None, mailer=DummyMailer())

No behaviour, no assertions. Its only job is to exist.

Stub — returns canned answers. No assertions on how it was called.

python
class StubRates:
    def get(self, currency): return Decimal("1.25")

Use when the test needs the collaborator to return something so the code under test can proceed. Most tests want this.

Spy — a stub that records how it was called, checked afterwards.

python
mailer = SpyMailer()
service.place_order(order)
assert mailer.sent == [("customer@example.com", "order_confirmation")]

Use when the call itself is the behaviour being tested — sending an email, publishing an event, writing an audit record.

Mock — strictly, a double with expectations set in advance, which fails if they are not met.

python
mailer.expect_send(to="customer@example.com", template="order_confirmation")
service.place_order(order)
mailer.verify()

The distinction from a spy is when the assertion is declared. In practice most frameworks blur this and most people mean "spy" when they say "mock."

Fake — a real, working implementation, simplified.

python
class InMemoryOrderRepo:
    def __init__(self): self._rows = {}
    def save(self, o): self._rows[o.id] = o
    def get(self, id): return self._rows.get(id)

Real behaviour, no infrastructure. The most underused of the five and often the best.

how to choose#

**Does the test care what the code does to the collaborator?** → Spy. Sending, publishing, recording.

**Does the test just need the collaborator to answer?** → Stub. This is most cases.

Is the collaborator stateful, and does the test exercise several operations? → Fake. Stubbing a sequence of returns for a stateful thing is where tests become unreadable.

Is it there only to satisfy a constructor? → Dummy.

the failure mode to avoid#

Over-verification. A test that asserts on every call to every collaborator is a test that fails whenever the implementation changes, even when the behaviour did not.

python
# brittle: asserts the mechanism
mock_repo.assert_called_once_with(id=42)
mock_cache.assert_called_once_with("order:42")
mock_logger.assert_called_with("fetching order")

# durable: asserts the outcome
assert service.get_order(42).total == Money(1250)

The second survives a refactor that changes the caching strategy. The first does not, and it will be deleted rather than updated the first time someone touches the code — which means it was never really a safety net.

Assert on the outcome. Verify calls only when the call is the outcome.

the case for fakes#

For anything with meaningful state — a repository, a queue, a clock, a filesystem — write a fake once and use it everywhere.

python
class FakeClock:
    def __init__(self, now): self._now = now
    def now(self): return self._now
    def advance(self, delta): self._now += delta

Now a test for expiry logic reads like the behaviour it tests:

python
clock = FakeClock(t0)
token = issue_token(clock)
clock.advance(hours=25)
assert not token.valid_at(clock.now())

No mock setup, no call verification, no rewriting when the implementation changes. The fake is a small amount of test infrastructure that pays for itself across dozens of tests.

The strongest signal that fakes are the right call: if maintaining a fake feels like too much work for a collaborator, that collaborator's interface is probably too large — which is a design finding, delivered for free.

the one rule#

Do not mock what you do not own.

Stubbing a third-party client's exact call signature couples your tests to their API. When they change it, your tests still pass and production breaks — which is the worst possible outcome for a test.

Wrap the third party in a thin interface you define, fake that, and cover the real integration with a small number of contract tests that actually call it.

— Dom, October 15, 2026

get README in your inbox

One dispatch, no noise. Tech and developer news, plus the occasional long piece on the craft.

subscribe →