Test Doubles
Learning Objectives
Distinguish dummy, stub, fake, spy and mock.
Choose the right double and avoid over-mocking.
Write contract tests that run against both real implementations and fakes.
Prerequisites: Modules 22, 64.
Intuition and Definition
Fills a parameter; never used nullptr logger that is never called Returns canned answers clock that always says 09:00 Working, simplified implementation
Records how it was called, for later assertions notifier that stores sent messages
Pre-programmed expectations, verifies interactions "expect charge(42.0) exactly once"
Why It Matters
Doubles isolate the unit under test from slow, non-deterministic or unavailable collaborators (databases, networks, hardware, time) — but every double is an assumption about the real thing.
Syntax and Language Notes
from unittest.mock import Mock
gateway = Mock()
gateway.charge.return_value = True
service.checkout(gateway, 42)
gateway.charge.assert_called_once_with(42)Complete Example
A contract test — the same test suite runs against the fake and the "real" implementation, so the fake can't drift from the real behaviour:
#include <functional>
#include <iostream>
#include <map>
#include <memory>
#include <optional>
#include <string>
struct Repository { // the contract
virtual ~Repository() = default;
virtual void save(const std::string& vin, int km) = 0;
virtual std::optional<int> find(const std::string& vin) const = 0;
};
struct InMemoryRepository : Repository { // FAKE used by unit tests
std::map<std::string, int> data;
void save(const std::string& v, int km) override { data[v] = km; }
std::optional<int> find(const std::string& v) const override { auto it = data.find(v); if (it == data.end()) return std::nullopt; return it->second; }
};
struct FileRepository : Repository { // stand-in for the real database adapter
std::map<std::string, std::string> rows; // pretend: rows as text
void save(const std::string& v, int km) override { rows[v] = std::to_string(km); }
std::optional<int> find(const std::string& v) const override { auto it = rows.find(v); if (it == rows.end()) return std::nullopt; return std::stoi(it->second); }
};
// contract tests: every implementation must pass
…InMemoryRepository: passes the repository contract
FileRepository: passes the repository contract
spy recorded 1 message: V1 due for serviceStep-by-Step Walkthrough
contract() encodes the behaviour every Repository must have; it runs unchanged against both implementations.
Unit tests of services use the in-memory fake (fast, deterministic) with confidence that it behaves like the real adapter.
The spy records interactions; the test asserts on what was sent, not on internal calls.
Common Mistakes
Over-mocking: mocking every collaborator and verifying every call — tests mirror the implementation and break on harmless refactors.
Mocking types you don't own (third-party SDKs) — wrap them in your own adapter interface and mock that.
Mocking value objects — just create real ones.
Fakes that drift from the real implementation — use contract tests.
Trade-offs and Alternatives
Prefer real objects for values and pure logic, fakes for infrastructure (repositories, clocks, file systems), spies for outgoing notifications, and mocks sparingly for interactions that *are* the behaviour (e.g. "the payment must be charged exactly once").
Real-World Use
In-memory databases/repositories in unit tests, fake clocks, WireMock/MSW for HTTP, hardware simulators for drivers, contract testing between microservices (Pact).
Practice
Q1. (Easy) Which double has a working, simplified implementation?
Answer: A fake.
Q2. (Medium) Spy vs mock?
Answer: A spy records calls for assertions after the act; a mock is configured with expectations beforehand and verifies them (often failing immediately on unexpected calls).
Q3. (Advanced) How do contract tests prevent fake drift?
Answer: The same behavioural test suite runs against the fake and the real implementation (in integration test runs), so any difference in behaviour fails a test.
Interview Question
Q1. "When are mocks harmful?" Answer: When they replace things that are cheap and deterministic anyway (values, pure functions), when tests verify incidental interactions (call order, exact arguments that don't matter), and when they mock third-party types whose real behaviour differs. The result is brittle tests that pass while production fails. Use real objects where possible, fakes for infrastructure with contract tests, and mocks only for interactions that are the essential outcome.
Quick Quiz
Q1. Which double returns canned answers?
Answer: A stub.
Q2. Which double should replace a database in fast unit tests?
Answer: A fake (in-memory implementation).
Q3. Should you mock third-party APIs directly?
Answer: Generally no — wrap them in your own interface and mock that.
Summary
Dummy, stub, fake, spy, mock — choose by need; don't over-mock.
Contract tests keep fakes honest.
Related: Modules 22, 64, 66.