Test Doubles: Fakes, Stubs, Spies, and Mocks
Before you begin: Objects, interfaces or compatible methods, and tests.
A test double stands in for a real collaborator during a test. A fake database, a fixed clock, and a sender that records messages are all test doubles, but they do different jobs.
This article uses the classic distinctions: a stub supplies prepared answers; a fake has a simplified working implementation; a spy records interactions for later inspection; and a mock has expectations about interactions that will be verified. Tools and teams sometimes use these names differently, so describe the behavior when a label is ambiguous. 1
Four replacements, four purposes
Imagine testing receipt delivery without connecting to an actual mail provider. We may need an address source and a message sender. The following small doubles show the differences without a mocking library.
// Stub: always supplies the answer chosen by the test.
public sealed class FixedAddress
{
public string Lookup(int customerId) { return "mira@example.test"; }
}
// Fake: remembers records and performs real lookup, but only in memory.
public sealed class MemoryDirectory
{
private readonly Dictionary<int, string> addresses = new();
public void Save(int id, string address) { addresses[id] = address; }
public string Lookup(int id) { return addresses[id]; }
}
public interface ISender
{
void Send(string message);
}
// Spy: records calls; the test decides afterward what they mean.
public sealed class RecordingSender : ISender
{
public List<string> Messages { get; } = new();
public void Send(string message) { Messages.Add(message); }
}
// Mock: the expected interaction is provided before the operation.
public sealed class ExpectedSender : ISender
{
private readonly string expected;
private int calls;
public ExpectedSender(string expected) { this.expected = expected; }
public void Send(string message)
{
if (message != expected) throw new Exception("Unexpected message.");
calls++;
}
public void Verify()
{
if (calls != 1) throw new Exception("Expected exactly one send.");
}
}
public static class Receipts
{
public static void Send(ISender sender) { sender.Send("Receipt ready"); }
}var spy = new RecordingSender();
Receipts.Send(spy);
if (!spy.Messages.SequenceEqual(new[] { "Receipt ready" }))
throw new Exception("Wrong receipt messages.");
var mock = new ExpectedSender("Receipt ready");
Receipts.Send(mock);
mock.Verify();The spy exposes a record. The test examines that record after the operation. The mock was configured with the required message first and has its own verification step. Forgetting Verify would let the zero-call case pass unnoticed.
class FixedAddress:
def lookup(self, customer_id):
return "mira@example.test"
class MemoryDirectory:
def __init__(self):
self._addresses = {}
def save(self, customer_id, address):
self._addresses[customer_id] = address
def lookup(self, customer_id):
return self._addresses[customer_id]
class RecordingSender:
def __init__(self):
self.messages = []
def send(self, message):
self.messages.append(message)
class ExpectedSender:
def __init__(self, expected):
self._expected = expected
self._calls = 0
def send(self, message):
assert message == self._expected, "Unexpected message."
self._calls += 1
def verify(self):
assert self._calls == 1, "Expected exactly one send."
def send_receipt(sender):
sender.send("Receipt ready")spy = RecordingSender()
send_receipt(spy)
assert spy.messages == ["Receipt ready"]
mock = ExpectedSender("Receipt ready")
send_receipt(mock)
mock.verify()The fixed address is a prepared answer, not a directory. The memory directory actually stores values but has none of a database’s persistence guarantees. The sender doubles either record what happened or verify an interaction chosen beforehand.
Choose the double for the question
Use a stub when a collaborator’s answer is simply an input to the behavior being tested. Use a fake when a sequence of meaningful operations is easier to express with working state than with many canned responses. Use a spy or mock when an interaction itself is important—for example, whether one receipt was requested rather than none or two.
The one-call check does not prove that an email reached a person. It establishes that this code requested a send from its collaborator. Delivery requires separate integration evidence.
A fake can also disagree with the real dependency. An in-memory map might compare names differently from a production database, never encounter transaction conflicts, and ignore durability. Keep tests against the real adapter for those promises.
Avoid making every internal call a requirement
Verifying every private collaboration can make harmless reorganization break dozens of tests. Prefer observable results unless a particular interaction is part of the requirement. Renaming an internal helper should not mean the product became incorrect.
A double is useful because it lets a test control or observe something deliberately. It is not a badge of testing sophistication. Sometimes the real collaborator is already small, deterministic, and easier to use.
Sources
- Martin Fowler — Mocks Aren’t Stubs, using the Meszaros test-double terminology.
- Python — unittest.mock, a library that can supply several of these behaviors.