Skip to content

Lesson 6 of 9 · 20m

Test Doubles: Mocks, Stubs, and Fakes

The right double for the right job — mocks verify interaction, stubs return canned data, fakes provide real behavior, and over-mocking is the test smell that rots suites fastest.

Test doubles exist because real dependencies are slow, nondeterministic, or unavailable in a test environment. The mistake is defaulting to mocks — mock what you must, fake what you can, and real what you can afford.

The three kinds

  • Stub — returns canned data: PriceService.stub(get_price=10). Verifies your code handles the response.
  • Mock — records interactions and asserts them: mock.send.assert_called_once_with(email). Verifies how your code talks to the dependency.
  • Fake — a working implementation that isn't production-grade: an in-memory database, a local mail spool, a fake clock. Verifies behavior without touching the real thing.

The over-mocking trap

# testing implementation, not behavior:
mock_repo.save.assert_called_with(user)
mock_email.send.assert_called_once()
mock_cache.set.assert_called_once()
# ... your test passes even if the code is completely wrong

When every dependency is mocked, the test verifies the wiring matches the mock setup — not that the system works. Reserve mocks for side effects you can't verify otherwise (email actually sent, metric actually recorded) — and prefer asserting the observable outcome (the user got an email) over the interaction (send was called).

Fakes beat mocks for stateful dependencies

class FakeOrderRepo:
    def __init__(self): self.orders = {}
    def save(self, order): self.orders[order.id] = order
    def get(self, id): return self.orders.get(id)

A fake has real behavior — save then get returns what was saved, not MagicMock(). For a repository or cache, a fake is usually less code than the mock wiring and tests the same thing.

::exercise(pick-the-double)

Field notes

  • If the test setup is longer than the test, the dependency is too coupled — that's a design signal.
  • pytest-mock, unittest.mock, or library-specific fakes (requests-mock, responses) — pick one per dependency type and be consistent.
  • Mocking a real DB is usually wrong — the mock can't teach you SQL semantics; use a real database in a transaction or a fake.