A Mock is the reflexive choice for any collaborator, but it quietly fails the moment a test depends on the dependency remembering something: a repository double that should return what was just written returns a fresh child mock instead, and the test either passes vacuously or forces you to script every call with brittle side_effect lists. The fix is a decision, made at the constructor seam, between a hand-written fake — a real in-memory implementation of the interface — and a unittest.mock double. This guide frames that trade-off: constructor injection as the seam, when a fake's coherent state beats a Mock, and how the choice pins you to either state testing or interaction testing.
Prerequisites
- Python 3.8+ (
unittest.mock.AsyncMock, moderncreate_autospec). Examples target 3.11. unittest.mockfrom the standard library;pytestfor the test bodies.- Comfort with constructor injection as a design pattern. The Dependency Injection for Testability overview covers the seam in depth.
Solution
Expose the collaborator as a constructor parameter. Then the same class under test accepts a fake when state matters and a mock when only interactions matter.
from typing import Protocol
from unittest.mock import create_autospec
# The contract both the real impl, the fake, and the mock satisfy.
class UserStore(Protocol):
def save(self, user_id: int, name: str) -> None: ...
def load(self, user_id: int) -> str | None: ...
class UserService:
# Constructor injection: the dependency is explicit and swappable.
# A real default keeps production wiring unchanged.
def __init__(self, store: UserStore) -> None:
self._store = store
def rename(self, user_id: int, new_name: str) -> str:
current = self._store.load(user_id) # read existing state
if current is None:
raise KeyError(user_id)
self._store.save(user_id, new_name) # write new state
return new_name
# --- FAKE: a real in-memory implementation. Holds coherent state across calls.
class FakeUserStore:
def __init__(self) -> None:
self._data: dict[int, str] = {}
def save(self, user_id: int, name: str) -> None:
self._data[user_id] = name # actually stores
def load(self, user_id: int) -> str | None:
return self._data.get(user_id) # returns what was stored
def test_rename_with_fake_state_testing():
fake = FakeUserStore()
fake.save(1, "old") # arrange real state
service = UserService(store=fake) # inject the fake
service.rename(1, "new")
# STATE testing: assert on the fake's observable state afterward.
assert fake.load(1) == "new"
def test_rename_with_mock_interaction_testing():
# autospec binds the double to the UserStore signature (catches drift).
mock_store = create_autospec(UserStore, instance=True)
mock_store.load.return_value = "old" # script the read
service = UserService(store=mock_store) # inject the mock
service.rename(1, "new")
# INTERACTION testing: assert HOW the collaborator was used.
mock_store.load.assert_called_once_with(1)
mock_store.save.assert_called_once_with(1, "new")
Why this works
Constructor injection turns the dependency into a value the test supplies, so swapping a fake for a mock requires no patching and no knowledge of where the dependency is imported. A fake is a genuine implementation: its load returns whatever its save stored, so a read-modify-write flow behaves coherently and the assertion checks resulting state. A mock returns scripted values per call and records invocations, so it naturally supports interaction testing — but it has no memory linking a save to a later load, which is exactly why stateful flows want a fake.
Edge cases and failure modes
- A
Mockmakes a stateful read-after-write test lie.mock_store.load()returns a child mock unrelated to any earliersave(), so a test asserting "the saved value is readable" passes without exercising the contract. Use a fake when the behavior under test is the round-trip itself. - Over-specified interaction tests turn into change detectors. Asserting every call on a mock couples the test to the implementation; a harmless refactor (an extra cache read) breaks it. Assert only the interactions that are part of the guarantee, or switch to state testing with a fake.
- Fakes drift from the real interface silently. A hand-written fake can fall out of sync when the real class adds a method. Define the interface as a
Protocol(or build the fake from the same base) and consider ancreate_autospec(Interface, instance=True)smoke test so autospec strict mocking catches signature drift in the mock path. - Async collaborators need awaitable doubles. If the dependency is a coroutine interface, the mock must be an
AsyncMockand the fake's methods must beasync def; see Mock vs MagicMock vs AsyncMock — when to use each for picking the class. - Time and randomness are stateful dependencies too. A fake clock that advances deterministically usually beats a
Mockwhosenow()returns a static value; route it through the constructor and compare with freezing time: freezegun vs monkeypatch.
Frequently Asked Questions
When does a hand-written fake beat a Mock? Use a fake when the dependency holds state across calls — a store that must remember writes, a clock that advances — and your assertions check resulting state rather than which methods ran. A Mock returns canned values per call and does not maintain coherent internal state.
Should I inject the dependency in the constructor or patch it? Inject through the constructor when you own the class. The constructor seam makes the dependency explicit, lets the test pass either a fake or a mock, and avoids brittle patch targets. Reserve patching for third-party code you cannot route through init.
How do fakes and mocks differ for verification? Mocks support interaction testing: assert_called_with and call_count verify how the collaborator was used. Fakes support state testing: you assert on the fake's observable state after the action. Pick the style that matches what the behavior actually guarantees.
Related guides
- When constructor injection is not available, you must instead choose a patch target — see Where to Patch: Understanding mock.patch Targets.
- Bind the mock path to the real signature with autospec strict mocking so fakes and mocks share one contract.
- Choosing the mock class for the dependency is covered in Mock vs MagicMock vs AsyncMock — when to use each.
- For collaborators that are network calls, a fake transport often beats a mock — see Mocking Network and HTTP Calls.
← Back to Dependency Injection for Testability