Test doubles explained: mock vs. stub vs. fake vs. spy
A test double is any object that stands in for a real dependency so a test can run in isolation. The four you'll meet most often differ by what they actually do: a stub returns canned answers to calls, a fake is a lightweight but working implementation (like an in-memory database), a spy records how it was called so you can assert on it afterward, and a mock is pre-programmed with expectations and fails the test itself if the expected calls don't happen. Stubs and fakes help you verify state; spies and mocks help you verify behavior.
What is a test double?
"Test double" is the umbrella term coined by Gerard Meszaros in xUnit Test Patterns, by analogy with a stunt double who steps in for an actor. It covers five kinds of stand-in, ordered here from simplest to most demanding:
- Dummy — an object passed around but never actually used, just to fill a required parameter.
- Stub — returns hardcoded answers to the specific calls made during the test.
- Fake — a real, simplified implementation with working logic that takes shortcuts unsuitable for production.
- Spy — a stub that also records information about how it was called.
- Mock — pre-programmed with expectations that form a specification of the calls it should receive, and can fail the test on its own.
Most day-to-day confusion is between the middle four. The single most useful distinction: stubs and fakes support state verification (you check the end result), while spies and mocks support behavior verification (you check that certain interactions happened).
Stub vs. mock: what is the actual difference?
This is the pair people mix up most. A stub is passive input; a mock is an active assertion.
- A stub exists to feed your code. If a function needs the current exchange rate, a stub returns 1.09 every time so the rest of the logic can run. Your assertions target the output the code produced, not the stub.
- A mock exists to check an interaction. If the contract says "on checkout, charge the payment gateway exactly once," a mock is told to expect one charge() call and will fail the test if that call never comes, comes twice, or comes with the wrong amount.
Rule of thumb: if you would still assert something without the double present, you probably want a stub. If the double is the assertion, you want a mock.
Fake vs. stub: when do you need a working implementation?
Reach for a fake when a stub's hardcoded answers can't keep up with the behavior your test exercises. A stub for a user repository might return the same record for every ID; the moment your test writes a record and expects to read it back, that stub breaks down. A fake — an in-memory implementation of the same interface — actually stores and retrieves data, so it holds up across a realistic sequence of calls while staying fast and free of external services.
Common fakes include in-memory databases, a fake clock you can advance by hand, and a fake mail server that captures messages instead of sending them. The tradeoff: a fake is more code to maintain and can drift from the real dependency, so it needs its own contract tests to stay honest.
Spy vs. mock: recording calls vs. setting expectations
Spies and mocks both watch interactions, but they differ in when they judge.
- A spy records calls silently and lets you assert on them after the fact: "the logger was called once, with a message containing 'timeout'." Nothing fails until your explicit assertion runs, which keeps the test readable and the failure message specific.
- A mock sets expectations up front and verifies them for you. This is stricter and can catch missing calls you'd forget to assert on, but over-strict mocks that pin down every internal call make tests brittle.
In practice, prefer a spy plus a targeted assertion when you care about one or two specific interactions, and reserve mocks for genuine contracts where an omitted call is itself a bug.
Which test double should I use?
Work from the cheapest option upward and stop at the first that proves your claim:
- Use the real object when it's fast, deterministic, and side-effect-free. A double you don't need is just coupling you'll pay for later.
- Use a stub when the code only needs a value to proceed and you'll assert on the result.
- Use a fake when the interaction spans multiple calls with state, like read-after-write.
- Use a spy when you must confirm a specific call happened but don't want up-front strictness.
- Use a mock when a required interaction is part of the contract and its absence is a defect.
The anti-pattern to watch for is over-mocking: tests so tied to internal call sequences that any refactor turns them red even when behavior is unchanged. When a suite breaks constantly on safe refactors, the doubles — not the code — are usually the problem.
Test doubles and end-to-end testing
Doubles are indispensable for fast unit tests, but they share one blind spot: a double only knows the behavior you told it about. If the real dependency changes and your stub or mock doesn't, your tests stay green while production breaks — an escaped defect no amount of mocking would have caught. That's the case for a layer of tests that run against the real, assembled system.
This is where AutoSim fits: self-healing end-to-end tests that drive the actual application, so you get the honesty of real dependencies without hand-maintaining brittle selectors. Pair fast, well-chosen doubles at the unit level with real end-to-end coverage above them, and you catch both the logic bugs and the integration bugs. For more on keeping that top layer stable, see our guides on fixing flaky end-to-end tests and writing resilient test selectors, or browse the full Klavity blog.
Key takeaways
- Pick the simplest test double that proves the test's actual claim
- Use stubs and fakes to verify state; use spies and mocks to verify behavior
- Prefer a fake over a mock when a lightweight real implementation exists
- Stop mocking internal calls that aren't part of the contract
FAQ
Is a mock the same as a stub?
No. A stub returns canned responses so your code has something to work with, and you assert on the resulting state afterward. A mock is pre-set with expectations about which calls should happen and fails the test itself if they don't — it verifies behavior, not state.
What is the difference between a fake and a stub?
A stub returns hardcoded answers for specific calls and has no real logic. A fake is a working but simplified implementation — an in-memory database or a fake payment gateway — that behaves like the real dependency while staying fast and self-contained.
When should I avoid mocks?
Avoid mocks when they force you to assert on internal call sequences that aren't part of the contract. That couples the test to implementation details, so a harmless refactor breaks a green test. Prefer real objects or fakes and verify observable outcomes instead.
Why do testing libraries use these terms differently?
The formal taxonomy comes from Gerard Meszaros's book xUnit Test Patterns. Frameworks blur it: Jest's jest.fn() acts as both a spy and a mock, while Sinon keeps spies, stubs, and mocks as separate APIs. Learn the concepts first, then map them onto your tool.
Catch bugs the moment a human sees them
Klavity: right-click bug reports, AI personas that review your product, and self-healing tests.
Get started free