Mock vs Stub vs Spy: Test Doubles Explained
Mock, stub, spy, fake, dummy — the words get used interchangeably and it costs you clarity in code review. Here is Gerard Meszaros's precise taxonomy, the state-versus-behavior split Martin Fowler drew, and when each one earns its place.
Almost everyone calls every test double a "mock." It is a harmless habit right up until a code review where one person means "an object that returns a fixed value" and another means "an object that fails the test if it isn't called." Those are different tools with different consequences, and the sloppy vocabulary hides real design decisions.
The good news is the terminology was pinned down years ago. Gerard Meszaros gave us the umbrella term test double and five named categories in xUnit Test Patterns, and Martin Fowler popularized the distinctions in his essay Mocks Aren't Stubs. This guide walks the five, each with a code example, then draws the one conceptual line that actually changes how you test.
What is a test double?
A test double is any object that stands in for a real dependency during a test, the way a stunt double replaces an actor. Gerard Meszaros coined the term and split it into five kinds — dummy, stub, spy, mock, and fake — which differ by how much behavior they carry and whether they verify state or interactions.
You reach for a double when the real collaborator is slow, non-deterministic, expensive, hard to set up, or simply not the thing under test — a payment gateway, a clock, a mailer, a database. "Test double" is the generic word; mock is one specific member of the family, not a synonym for the whole thing. Meszaros's five differ along two axes: how much real behavior they implement, and whether they exist to feed the code inputs or to observe its outputs. Keep those two questions in mind as we go.
The five test doubles
Dummy — fills a slot, never used
A dummy is the emptiest double. It is passed around to satisfy a parameter list but its value is never actually exercised on the path you are testing. It exists purely to let you construct the object under test.
// The constructor requires a logger, but this test path never logs.
// We pass a dummy just to satisfy the signature.
const dummyLogger = {};
const cart = new ShoppingCart(dummyLogger);
cart.addItem({ id: 'sku-1', price: 10 });
expect(cart.total()).toBe(10); // logger never touchedStub — canned answers (indirect input)
A stub is pre-programmed to return fixed, canned answers to the calls made during the test. Its job is to provide indirect input — to feed the code under test whatever it needs from a collaborator so you can drive a specific path. The ISTQB glossary defines it as a skeletal implementation used to supply the caller with required responses. You never assert on the stub itself; you assert on what the code does with its answers.
// Stub returns a canned exchange rate so we can test conversion logic
// without hitting a live currency API.
const rateStub = {
getRate: () => 0.9, // always returns this, regardless of args
};
const converter = new PriceConverter(rateStub);
// We verify the RESULT (state), not how the stub was called.
expect(converter.toEur(100)).toBe(90);Spy — a stub that records how it was called
A spy is a stub with a notebook. It still returns canned answers, but it also records how it was called — which methods, with what arguments, how many times. You inspect that record after the fact to assert the interaction happened. The verification is retrospective: exercise the code first, then check what the spy captured.
// Spy records every call so we can assert on it afterward.
const emailSpy = {
calls: [],
send(to, subject) {
this.calls.push({ to, subject }); // record
return true; // canned answer
},
};
const service = new SignupService(emailSpy);
service.register('ada@example.com');
// Verification happens AFTER the fact, on the recorded calls.
expect(emailSpy.calls).toHaveLength(1);
expect(emailSpy.calls[0].to).toBe('ada@example.com');Mock — expectations that self-verify
A mock is pre-programmed with expectations: a specification of exactly which calls it should receive, with which arguments, in what order. The mock verifies behavior — if the expected interaction never happens, the mock itself fails the test. This is the key difference from a spy: a spy passively records and lets you assert later; a mock actively carries the assertion and enforces it. Fowler's essay uses this behavior-verifying object as the definition of a true mock.
// Mock is set up with an EXPECTATION before the code runs.
const mailer = mock(Mailer);
mailer
.expects('send')
.once()
.withArgs('ada@example.com', 'Welcome');
const service = new SignupService(mailer);
service.register('ada@example.com');
// The mock verifies the expectation itself — this FAILS the test
// if send() was never called with those exact args.
mailer.verify();Fake — a working, simplified implementation
A fake has real, working behavior — just a shortcut version not fit for production. The classic example is an in-memory database that genuinely stores and queries data but keeps it in a map instead of on disk. A fake actually does the thing, which is what separates it from a stub's canned answers. You still verify state — you inspect the fake's contents or the result — but the logic in between is real.
// Fake: a real, working repository backed by a Map instead of Postgres.
class InMemoryUserRepo {
#users = new Map();
save(user) { this.#users.set(user.id, user); }
findById(id) { return this.#users.get(id); }
}
const repo = new InMemoryUserRepo();
const service = new UserService(repo);
service.createUser({ id: 'u1', name: 'Ada' });
// State verification against a fake that genuinely persisted the data.
expect(repo.findById('u1').name).toBe('Ada');The one split that matters: state vs behavior
If you remember one thing, make it this. Fowler's central insight in Mocks Aren't Stubs is that the doubles cluster into two styles of verification:
- State verification — exercise the code, then check the result: a return value or the state of an object afterward. Stubs and fakes support this style; they supply inputs and stay out of the way.
- Behavior verification — check the interactions: assert that the code called its collaborators correctly. Spies and mocks support this style; they observe or enforce the calls.
Neither is universally right. State verification asks "what came out?" and tends to survive refactoring, because it cares about outcomes, not mechanics. Behavior verification asks "what happened along the way?" and is the tool you need when the outcome is an interaction — an email that must be sent, a payment that must be charged, a cache that must be invalidated — with no return value to inspect.
Stub vs spy vs mock vs fake, side by side
| Feature | Stub | Spy | Mock | Fake |
|---|---|---|---|---|
| Purpose | Feed indirect input | Record calls to assert later | Enforce expected calls | Real logic, simplified |
| Returns canned values? | ✓ | ✓ | ✓ | computes real ones |
| Records calls? | — | ✓ | ✓ | — |
| Has expectations? | — | — | ✓ | — |
| Verifies | state (you assert) | behavior (you assert after) | behavior (self-verifies) | state (you assert) |
The names are blurred in real libraries
Meszaros's taxonomy is crisp; the tools are not. In practice, popular libraries collapse these categories. Jest gives you jest.fn() and jest.spyOn() that are really configurable spies with stub-like return control — you use the same object to stub returns and to assert calls. Sinon at least keeps three words — stub, spy, mock — but a Sinon stub can also record calls, which by Meszaros's definitions makes it a spy too. So do not expect the API name to map cleanly onto the concept. The useful move is to name the role the double plays in a given test — "here it's feeding input," "here it's verifying an interaction" — rather than trusting the method name. The taxonomy is a thinking tool, not an API contract.
When to use which — and the over-mocking trap
A workable default: prefer real objects; use stubs or fakes to supply inputs and verify state; reach for spies or mocks only when the behavior you care about is the interaction and there is no result to inspect. Save dummies for filling parameter lists you don't touch.
The trap is over-mocking. Every mock encodes an assumption about how your code talks to its collaborators, so a heavily mocked test is welded to the current implementation. Refactor the internals — split a method, reorder two calls, swap a collaborator — and a pile of tests go red even though the observable behavior never changed. Worse, the coupling cuts the other way: a suite of green, fully mocked unit tests tells you the pieces work in isolation against your assumptions, not that they work together. The real, un-mocked integration can be broken while every mock happily plays its scripted part.
This is exactly why the testing pyramid wants fewer, more integrated tests sitting above the mock-heavy unit layer: something has to exercise the wiring the mocks stubbed out. It is also why mock-driven design pairs naturally with test-driven development, where you use doubles to describe collaborations before the collaborators exist — but you still need higher-level tests to prove the collaborations are real. And it is a reminder that automated checks are one layer, not the whole story: see test automation vs manual testing for where each fits.
When the un-mocked path is what breaks
The unit suite is green. Every mock returned exactly what it was told to. And a user still hits a wall on the real checkout page, where the actual payment client, the real clock, and the live feature flags interact in a combination no mock ever rehearsed. Those escaped bugs — the ones that only surface once the doubles are gone — are precisely the ones a passing test can't explain. What closes them fast is a captured reproduction: the session replay, console error, and failing network request from the moment it broke, so the fix targets the real integration rather than another guess. That is the gap BugMojo fills — it records the un-mocked path your unit tests never saw.
When a green test suite ships a broken integration, install the free BugMojo extension to capture a session replay plus console and network logs in one click — a reproduction the fix can actually target.
Install the extensionFrequently asked questions
Frequently asked questions
Sources
- Martin Fowler, 'Mocks Aren't Stubs' — the canonical essay defining state vs behavior verification and the test-double family — martinfowler.com (2026)
- Wikipedia — Test double: overview of dummy, stub, spy, mock, and fake — Wikipedia (2026)
- Martin Fowler, TestDouble — the umbrella term coined by Gerard Meszaros and the five categories — martinfowler.com (2026)
- ISTQB Glossary — formal definition of a test stub — ISTQB (2026)
Get bug-tracking insights, weekly.
Engineering deep-dives, QA playbooks, and honest tool comparisons. No spam — unsubscribe in one click.

