Last updated: 2026-09-16

U
Undergraduate level

Test Doubles: Mocks, Stubs, Fakes, and When You Can't Just Call the Real Thing

Mock" gets used as a catch-all word for "not the real dependency," which flattens five genuinely different tools into one, and loses the distinction that actually matters: what kind of question a test can ask once the stand-in is in place. This page separates them out, then covers a reason to reach for one that has nothing to do with speed or flakiness — the real dependency would do something in the world that cannot be undone — and finishes with a question that shapes which of these tools a codebase ends up full of without anyone deciding it on purpose: whether the team is building top-down, bottom-up, or from the middle out.

The five kinds of test double

Gerard Meszaros's pattern catalogue names five distinct roles, and Martin Fowler's widely-read clarification of the same taxonomy draws the one distinction that matters most between them12:

  • Dummy — passed around but never actually used. It exists purely to fill a parameter slot a method signature demands, on a code path the test never exercises.
  • Stub — provides canned answers to calls made during the test, and nothing else; it doesn't respond meaningfully to anything not explicitly scripted in. A stub controls what indirect input the code under test receives.
  • Spy — a stub that also records how it was called, so the test can inspect that record afterward: how many times, with what arguments, in what order.
  • Mock — pre-programmed with expectations about the calls it should receive, and verifies those expectations itself, typically failing the test directly if they weren't met.
  • Fake — a real, working implementation that just isn't fit for production: an in-memory database standing in for a real one, or a genuinely functioning but simplified algorithm.

Fowler's distinction cuts across all five: a mock does behaviour verification — it checks that the right calls happened, in the right way, as the test runs. Everything else on this list is checked by state verification instead — the test lets the code under test run, then inspects what actually resulted, after the fact. Reaching for "mock" as the name for all five hides which kind of check a test is actually making, and a state-verification test dressed up in mocking-framework syntax asserting on internal calls it doesn't need to care about is a common, avoidable source of brittle tests that break on a harmless refactor.

The everyday reason: speed and control

Most of the time, a double exists because the real dependency is slow, external, or not yet built. Testing Fundamentals and Testing Non-Deterministic Systems cover that territory in depth — controlling the clock, the network, and randomness so a test is fast and deterministic. This page is about the other reason.

The other reason: the real thing has a permanent effect

Some dependencies aren't just slow to call in a test — they cannot be called at all, because calling them does something in the world a test has no business doing: sending a real email or text message, charging a real card, posting to a real public account, deleting production data, triggering a webhook to a third party who now believes something happened, or driving a physical actuator. For this category, a double isn't a convenience for speed. It's the only safe way to exercise that code path outside a deliberately provisioned sandbox — and a vendor's sandbox/test-mode API keys are themselves just a fake someone else wrote and maintains, not a reason to skip having your own.

This site's own PatLang runtime draws this line inside its virtual filesystem. vfs_read/vfs_write/vfs_delete and the rest of the vfs_* family are a safe, in-memory fake available on every target including the browser's WASM sandbox, where there is no real filesystem access at all — a program can call them as many times as it likes with no consequence outside its own process. The one function that actually touches a real file, vfs_flush_to_disk, is deliberately not something vfs_write quietly does underneath — it's a separate, explicitly-named, native-only call, gated by an allowed-root check, so the irreversible operation looks different in the code from the safe one and can't be reached by accident. See the VFS demo for the working example.

That's the design lesson underneath the testing one: when you own the interface to an irreversible action, shape the signature so a test literally cannot trigger it by mistake. Separate "compose the message" from "send the message" into two calls, and inject the sender as a dependency the test replaces — rather than letting some deeply-buried internal line reach for a real SMTP connection or payment API directly. A codebase designed this way needs no special discipline in its test suite at all; the irreversible path is simply not reachable from where a test lives.

Contract tests: keeping the double honest

A double is only as good as its fidelity to the real dependency's actual behaviour. If the real API changes shape and the stub or fake doesn't change with it, every test built on that double keeps passing while the real integration quietly breaks underneath. Types of Testing already names the mitigation: a smaller, separate suite of contract tests (tools like Pact are built for exactly this) that runs against the real service, or its vendor-provided sandbox, on its own schedule — checking periodically that the assumption a double encodes still matches reality, rather than trusting it forever once it's written.

Which double for which direction: top-down, bottom-up, and sandwich

The kind of double a codebase accumulates isn't only a testing decision. It follows from which direction the system is actually being built in, and the ISTQB testing glossary names the three standard shapes3:

  • Top-down. Start from the highest-level, user-facing module and integrate downward. Lower modules that don't exist yet are replaced with stubs, so the overall structure and user-facing behaviour can be validated early — an advantage, since a design mistake at the top is the most expensive kind to find late. The risk sits entirely in the stub: it's a hypothesis about how the real lower module will eventually behave, and if that hypothesis is wrong, the top-level test passes for the wrong reason until the stub is finally replaced with the real thing.
  • Bottom-up. Start from the lowest-level modules — the ones with no unfinished dependencies of their own — and integrate upward using real, already-verified components underneath each new layer. The not-yet-built caller above a module is replaced with a driver: a harness that exists purely to invoke the module under test the way its eventual caller will. The advantage is the mirror image of top-down's weakness: very little guessing about how a real dependency behaves, since the dependencies below are already there. The risk is also the mirror image: the user-facing behaviour isn't exercised until very late, so a misunderstood requirement at the top surfaces last, not first.
  • Sandwich (middle-out). Pick a middle layer and work in both directions from it at once — stubs going down for what isn't built yet underneath, drivers going up for what isn't built yet above. This is the common shape for a system being built by more than one team in parallel on different layers simultaneously. It needs more coordination than committing to one direction, but avoids fully inheriting either direction's single biggest weakness.

None of this is usually a decision a team writes down. It's closer to a default that falls out of which module happens to exist first, and it quietly answers "what kind of double do we reach for here?" before anyone asks the question directly — a codebase that's mostly grown top-down accumulates stubs by habit, one grown bottom-up accumulates drivers, and one built sandwich-style needs both and should expect to. Noticing which direction a codebase is actually being built in explains, more reliably than any written test-strategy document, which kind of double keeps turning up in its suite.

A practical checklist

  • Name the double by what it actually does — dummy, stub, spy, mock, or fake — not "mock" for all five; the name determines whether a state check or a behaviour check is the right thing to assert.
  • If calling the real dependency would do something irreversible, make that specific call impossible to reach by accident: a separate function, an explicit gate, a dependency the test can replace.
  • Treat a double as a hypothesis about the real dependency's behaviour, and check that hypothesis periodically with a contract test against the real thing or its vendor sandbox.
  • Notice whether the codebase is growing top-down, bottom-up, or sandwich-style — it explains which kind of double is piling up in the suite, and whether that's actually the right one for where the design risk currently sits.

Where this connects

  • Testing Fundamentals — the general mocking and property-based testing foundations this page assumes.
  • Testing Non-Deterministic Systems — the speed/flakiness reason for a double, covered in depth, alongside the same cassette-style real-vs-replayed pattern this page's contract-test section echoes.
  • Types of Testing — where contract testing sits in the wider pyramid, and the pyramid's own top-down/bottom-up-adjacent trade-offs.
  • PatLang Virtual Filesystem — the working fake-vs-gated-real example this page's irreversibility section is built on.

References


  1. Meszaros, G. (2007). xUnit Test Patterns: Refactoring Test Code. Addison-Wesley Professional. https://www.oreilly.com/library/view/xunit-test-patterns/9780131495050/

  2. Fowler, M. (2007). Mocks Aren't Stubs. https://martinfowler.com/articles/mocksArentStubs.html

  3. ISTQB. Standard Glossary of Terms Used in Software Testing. https://glossary.istqb.org/