Test doubles taxonomy
“Mock” is colloquially used for anything fake in a test. Gerard Meszaros formalized five distinct kinds, each with its own purpose. Knowing which one you actually need clarifies the design.
The five kinds
| Kind | Purpose | Example |
|---|---|---|
| Dummy | Fills a parameter, never used | User(audit_log=DummyLogger()) where audit isn’t called |
| Stub | Returns canned answers | repo.get.return_value = User(id=1) |
| Spy | Stub + records calls for inspection | mock.assert_called_with(...) after the test |
| Mock | Pre-programmed with expectations; fails if violated | expect(repo).to_receive(:save) (RSpec-style) |
| Fake | Working implementation, simpler than real | In-memory database, fakeredis |
The Python unittest.mock library calls everything Mock regardless of role — confusing nomenclature but practical.
Dummy
A placeholder. The test path doesn’t actually invoke it.
class DummyLogger:
def info(self, *args, **kwargs): pass
def error(self, *args, **kwargs): pass
def test_user_signup():
service = SignupService(logger=DummyLogger())
user = service.signup("alice@example.com")
assert user.email == "alice@example.com"
Use when a constructor demands an argument the test doesn’t care about. Real codebases often use Mock() instead — works, but loses the “this is intentionally never called” signal.
Stub
Returns canned values. No verification.
def test_get_user_returns_profile():
repo = Mock()
repo.find.return_value = User(id=1, name="alice")
service = UserService(repo)
profile = service.get_profile(1)
assert profile.name == "alice"
The test’s interest is in service.get_profile’s output. The stub provides input. Don’t assert on repo.find.called — that’s testing the implementation, not the behavior.
Spy
Stub + recorded calls. The test asserts on what was called.
def test_save_audit_log():
audit = Mock()
service = OrderService(audit_log=audit)
service.place_order(items=[...], user_id=42)
audit.record.assert_called_once_with(
event="order_placed", user_id=42, item_count=...
)
The test cares that the audit log received the call. Spy = behavior verification.
Mock (in the strict sense)
Pre-programmed expectations: declare what calls are expected before running, and the framework fails if they don’t happen.
# Strict-mock style (jMock, rspec)
expect(repo).to_receive(:save).with_arg(user).once
service.create_user(user)
# Framework verifies on teardown
Python’s unittest.mock doesn’t enforce this strictly — you have to call assert_called_* manually. So “mock” in Python usually means “spy.”
Fake
A working implementation, simpler than the real one. Behaves correctly for the operations the test exercises.
class InMemoryUserRepo:
def __init__(self):
self._users = {}
def save(self, user):
self._users[user.id] = user
def find(self, id):
return self._users.get(id)
def test_create_and_find():
repo = InMemoryUserRepo()
service = UserService(repo)
user = service.create("alice")
assert service.get(user.id).name == "alice"
Fakes pay off when:
- Multiple tests exercise the same component.
- The interactions are complex (multiple calls, ordering matters).
- The real dependency is slow or hard to set up (DB, network).
The cost: you have to maintain the fake’s correctness. When the real component changes, the fake can lie. Mitigate with contract tests (see below).
Famous fakes: fakeredis, moto (AWS), httpretty / responses (HTTP), SQLite-as-Postgres (sometimes risky — different SQL dialects).
“Don’t mock what you don’t own”
Rule of thumb attributed to Steve Freeman: only mock interfaces you control.
Bad:
@patch("requests.get")
def test_fetch(mock_get):
mock_get.return_value.json.return_value = {"ok": True}
...
Why bad: requests is third-party. If it changes return semantics or adds a header you don’t anticipate, your mock disagrees with reality. Tests pass; production breaks.
Good: wrap third-party libs behind your own interface, then mock the interface.
# myapp/http.py
class HttpClient(Protocol):
def get(self, url: str) -> dict: ...
class RealHttpClient:
def get(self, url: str) -> dict:
return requests.get(url).json()
# In tests:
class FakeHttpClient:
def get(self, url: str) -> dict:
return {"ok": True}
Now the mock matches an interface you defined. Changes to requests are localized to RealHttpClient.
Contract tests
If you use a fake to substitute for a real service, write a contract test that runs the same scenarios against both. If the real service changes, the contract test fails first — long before a bug ships.
@pytest.fixture(params=[InMemoryUserRepo, PostgresUserRepo])
def repo(request):
return request.param()
def test_save_and_retrieve(repo):
user = User(id=1, name="alice")
repo.save(user)
assert repo.find(1).name == "alice"
Run the test against both implementations.
testcontainers — real dependencies in CI
For when fakes are too lossy and you want a real dependency without managing containers manually:
from testcontainers.postgres import PostgresContainer
@pytest.fixture(scope="session")
def postgres():
with PostgresContainer("postgres:16") as pg:
yield pg.get_connection_url()
Pros: real Postgres, real driver, real SQL behavior. Cons: slower (container startup), needs Docker in CI.
Use for the small set of integration tests where SQL semantics matter (locks, isolation levels, dialect-specific features). Use fakes everywhere else.
When to use which
- Pure unit test — stub or fake. Avoid spies unless behavior verification is the point of the test.
- Component test of A’s interaction with B — spy on B (verify A calls B correctly).
- Component test of A’s behavior — fake B (verify A’s output, not A’s calls).
- Integration test — testcontainers, real DB.
- End-to-end — real services, smoke-test only.
Interview angle
- Q: “What’s the difference between a stub and a mock?” — stub returns values; mock verifies calls.
- Q: “What’s a fake and when do you prefer it over a mock?” — working implementation; better for complex interaction tests, single source of truth.
- Follow-up: “What’s the ‘don’t mock what you don’t own’ rule?” — wrap third-party libs in your own interface; mock the interface.
- Follow-up: “How do you keep a fake honest?” — contract tests against the real implementation.
See 05_mock_vs_magicmock_patch.md, 01_mocks_external_apis.md.