Test Doubles
Dummy, stub, spy, mock and fake — Meszaros's five kinds of test double, what each is for, and why using the words precisely makes code reviews shorter.
3 min read · updated 19 September 2026
Gerard Meszaros named five in xUnit Test Patterns, and the names have held up. Most confusion in testing conversations comes from "mock" being used for all five.
| Kind | Has behaviour? | Test asserts on it? | Typical use |
|---|---|---|---|
| Dummy | no | no | fills a required parameter |
| Stub | canned answers | no | supplies input to the code under test |
| Spy | records calls | yes, afterwards | checks a side effect happened |
| Mock | pre-programmed expectations | yes, built in | strict interaction checking |
| Fake | real, simplified | usually on its state | replaces a slow dependency |
#Dummy
An object that is passed but never used. It exists because a constructor demands it.
// The logger is never called in this path; it is here to satisfy the signature.
const service = new OrderService(gateway, repository, null as unknown as Logger);If you find yourself writing dummies often, the constructor is doing too much. See writing testable code.
#Stub
Supplies input. The test does not care that it was called, only that the code under test received something specific to work with.
# Python. The clock is a stub: it exists so "now" is a known value.
class FrozenClock:
def now(self): return datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc)
def test_token_expires_after_an_hour():
token = issue_token("alice", clock=FrozenClock())
assert token.expires_at == datetime(2026, 1, 1, 13, 0, tzinfo=timezone.utc)The assertion is on the result, not the clock. That makes it a stub.
#Spy
Records what happened so the test can check afterwards. Use one when the side effect is the behaviour and there is no return value to assert on.
// TypeScript. A hand-rolled spy: no framework needed.
class RecordingMailer implements Mailer {
readonly sent: Message[] = [];
async send(message: Message) { this.sent.push(message); }
}
it('emails the customer once the order ships', async () => {
const mailer = new RecordingMailer();
await new Shipping(mailer).ship(order);
expect(mailer.sent).toHaveLength(1);
expect(mailer.sent[0].subject).toContain('on its way');
});Note how much this reads like a state assertion. That is deliberate, and it is why spies age better than mocks: the test says an email like this went out, not this method was called with these arguments.
#Mock
A double with expectations set in advance, which fails the test if they are not met — often including call counts and ordering.
// C#, Moq. Strict: any unconfigured call is a failure.
var gateway = new Mock<IPaymentGateway>(MockBehavior.Strict);
gateway.Setup(g => g.ChargeAsync(It.Is<ChargeRequest>(r => r.Cents == 4_000)))
.ReturnsAsync(ChargeResult.Succeeded("pi_1"))
.Verifiable();
await new Checkout(gateway.Object).PayAsync(order);
gateway.Verify();Mocks are the most precise and the most brittle. They are the right tool when the interaction itself is the contract — "we must call the fraud service before we capture the payment, and exactly once" is a real business rule and deserves a mock. They are the wrong tool for "the repository saved it", which is a statement about state dressed up as one about calls.
Strict mocks in particular should be rare. A strict mock fails when the code makes a call you did not anticipate, which means every new collaboration breaks every existing test.
#Fake
A real implementation, simplified. In-memory repositories, an SMTP server that collects mail in a list, SQLite standing in for Postgres, or WireMock serving recorded HTTP responses.
Fakes have genuine behaviour, which is their advantage and their risk: they can be exercised the way the real thing is, and they can drift from it. The standard mitigation is one shared test suite run against both the fake and the real implementation.
// Java. Same tests, two implementations — the fake cannot drift silently.
abstract class OrderRepositoryContract {
abstract OrderRepository repository();
@Test void saves_and_reads_back() { /* ... */ }
@Test void find_is_case_insensitive_on_reference() { /* ... */ }
}
class InMemoryOrderRepositoryTest extends OrderRepositoryContract {
OrderRepository repository() { return new InMemoryOrderRepository(); }
}
class PostgresOrderRepositoryTest extends OrderRepositoryContract {
// @Testcontainers — the real thing, one container for the class
OrderRepository repository() { return new PostgresOrderRepository(dataSource); }
}#Choosing
A workable default:
- Needs a value back, do not care that it was asked → stub
- Side effect is the behaviour, want to check it happened → spy
- The call sequence is itself the rule → mock
- Dependency is slow or has awkward setup, and you want real behaviour → fake
- Parameter you must supply and nothing touches → dummy
And one negative rule that resolves most reviews: if the test asserts on the double rather than on the outcome, ask what would break if the implementation changed shape. If the answer is "this test, and nothing else", the double is too strict.
Where to go next: unit testing with mocks for the isolationist case, without mocks for the sociable one, and mocking frameworks for the libraries that build these in each ecosystem.
Common questions
- What is the difference between a mock and a stub?
- A stub supplies canned answers so the code under test can proceed; the test never asserts on it. A mock is a stub that also records how it was called, and the test asserts on those recordings. The distinction is about where the assertion lives, not about which library made the object.
- What is a fake?
- A working implementation that is unsuitable for production — an in-memory repository, a Postgres running in a container, a mail sender that appends to a list. It has real behaviour, which is what separates it from a stub.
- Do these distinctions actually matter day to day?
- They matter in review. "This is a mock where a stub would do" is a precise, actionable comment that takes five words; without the vocabulary the same comment takes a paragraph and usually is not made.
Runnable samples for this page
last test results ↗- TypeScript
typescript/src/testing-levels/test-doubles
Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.
Was this page useful?
Related topics
- Unit Testing with MocksThe solitarist approach — isolate the unit behind test doubles. What it buys, the coupling it creates, and the three rules that keep a mock-heavy suite maintainable.
- Unit Testing without MocksThe sociable approach — real collaborators, state-based assertions, and tests that survive refactoring. What it buys, where it breaks down, and how to keep it fast.
- Mocking FrameworksMoq, NSubstitute, Mockito, unittest.mock, Sinon and the rest — what each ecosystem's mocking library does well, and the failure modes they share.
- MoqThe .NET mocking library in practical detail — setups, argument matchers, verification, loose versus strict behaviour, and what to use instead where it strains.
- Jest MockingModule mocks, spies, manual mocks and timer control — how Jest's mocking works, and the patterns that keep it from taking over a suite.
- WireMockA programmable HTTP server for tests — stubbing responses, injecting failures, verifying requests, and recording real traffic to replay.
- Terraform for Test InfrastructureProvisioning ephemeral test environments as code — per-branch stacks, workspaces, cost controls, and testing the Terraform itself.
- Writing Tests in C#The .NET testing stack — xUnit, Moq or NSubstitute, WebApplicationFactory, Testcontainers — and the dependency injection story that makes it the most testable of the four.