The Doubles, Clearly Defined
If you've ever been confused about the difference between a "mock" and a "stub," it's not you. The words get used loosely everywhere - most people say "mock" for any test double, and most mocking libraries are named for just one member of a larger family. Let's clear it up once, with plain definitions and a small example of each.
The classic naming comes from Gerard Meszaros's book xUnit Test Patterns, now the shared vocabulary most teams use (source: https://martinfowler.com/bliki/TestDouble.html, Martin Fowler's summary). There are five members. The single most useful way to keep them straight is to sort them by what they do:
does nothing ──────────────────────────────────► does a lot
+ checks you back
dummy stub fake spy mock
───── ──── ──── ─── ────
just fills returns a real-ish records a stub that
a slot canned working how it ALSO asserts
(never answers lite version was called it was called
used) (in-memory) correctly
The first three (dummy, stub, fake) only care about providing input to the code under test. The last two (spy, mock) also care about verifying output - the calls your code made. That input/output split is the real dividing line, and it matters more than the names. Hold onto it.
Dummy - fills a slot, never gets used
What it actually is. The emptiest possible double. A dummy is an object you pass only because the signature requires something there - but the code path under test never actually calls it. It exists to satisfy the compiler or the function signature, nothing more.
What it's for. Getting past a required parameter that's irrelevant to this particular test.
A small example.
// createOrder needs a `logger`, but the "rejects empty cart" path
// never logs anything - so any object will do.
const = ;
.;
What just happened: We handed createOrder an empty object as its logger purely to fill the argument
slot. The empty-cart check throws before any logging happens, so the dummy is never touched. If the code
did call dummyLogger.info(...), this would blow up - fair, since it would mean we picked the wrong kind
of double for this test.
⚠️ Gotcha - null is a trap dummy. Passing null as a dummy works only if you're certain the value
is never used. The moment a refactor makes the code touch it, you get a confusing null-reference crash
instead of a clear test failure. Many people pass an explicit empty object or a typed placeholder so the
intent ("this is deliberately unused") is visible.
Stub - canned answers on demand
What it actually is. A double that returns predetermined answers to the calls your code makes. It's the workhorse. A stub has no logic of its own; you tell it "when asked X, reply Y," and it does.
What it's for. Feeding your code a specific situation so you can test how it reacts. This is how you test the hard-to-reach paths from Phase 1: a stub can return an empty result, a giant result, or throw an error, on command.
A small example.
// We want to test: "what does our code do when the user has no orders?"
// Stub the repository to return an empty list, every time.
const = ;
const = await ;
.;
What just happened: The stub's findByUser ignores its argument and always returns [], forcing the
"no orders" situation deterministically without needing a real user who genuinely has none. We're testing
our dashboard logic's reaction to empty data - the stub just supplies it.
The same trick forces error paths:
const = ;
// Now we can test that buildDashboard falls back gracefully when rates are down.
What just happened: A real API won't fail on demand, but a stub throws reliably - so we can finally test the fallback code that only runs when things go wrong.
💡 Key point. A stub is about inputs to your code. You never assert anything on the stub itself - you assert on what your code did with the canned answer. If you find yourself checking "was the stub called?", you've crossed into spy/mock territory (below).
Fake - a real, working, lightweight version
What it actually is. A fake has a genuine working implementation - just a simpler, lighter one than production. The textbook example is an in-memory database: it really stores and retrieves data, with real logic, but it lives in a hash map instead of a server, so it's fast and disposable.
What it's for. When the dependency has enough behavior that canned answers get unwieldy - when your code writes and then reads back, or relies on the dependency actually behaving correctly across several calls.
A small example.
// A fake user repository: a real, working store backed by a Map.
const = ;
await ;
// It behaves like the real thing: what we saved, we can read back.
.;
What just happened: Unlike a stub, which would return a hard-coded answer regardless of what you saved,
the fake genuinely remembers state - save then findById works the way a real repository works. That
makes it ideal for testing flows that span several operations, without standing up a real database.
📝 Terminology - stub vs fake, the one-line version. A stub returns whatever you told it to, ignoring reality. A fake actually works - it has logic and (often) state. Reach for a fake when a stub would need so many canned answers that it stops being simpler than the real behavior.
Spy - records how it was called
What it actually is. A spy is a double that remembers how it was used - which methods got called, with what arguments, how many times - and lets you inspect that after the fact. It can wrap canned answers like a stub and keep a logbook.
What it's for. Verifying a side effect you can't see in the return value. The classic case: "did we actually send the welcome email?" The function's return value won't tell you; the only evidence is that the email sender was called.
A small example.
// A spy email sender: it records every call instead of sending.
const = ;
const = ;
await ;
// Now we inspect the logbook AFTER running our code.
.;
.;
What just happened: registerUser doesn't return anything about email, so we couldn't assert on the
return value. Instead the spy quietly recorded that send was called once, with Ada's address, and we
checked that record afterward. The spy answers "was this interaction triggered?"
Mock - a stub that also demands to be called correctly
What it actually is. A mock is the strictest member: it's a stub (canned answers) plus built-in expectations about how it will be called, set up before you run the code. If your code doesn't call it exactly as the mock was told to expect - wrong arguments, wrong number of times, wrong order - the mock itself fails the test.
What it's for. When the interaction is the behavior you care about - when "we must call the payment gateway exactly once, with this amount" is itself the thing the code is supposed to guarantee.
A small example (using a Jest-style mock):
const = ;
await ;
// The assertion IS about the interaction with the double.
.;
.;
What just happened: jest.fn() created a double that both returns a canned { id: "ch_1" } and records
its calls. We then asserted on the double directly: it must have been called once, with that exact token
and amount. The test's whole point is the interaction - charge the right card, the right amount, exactly
once (never double-charge).
Spy vs mock - the subtle one
People mix these up constantly, so here's the plain distinction:
| Spy | Mock | |
|---|---|---|
| When expectations are set | After the code runs - you inspect the record | Before the code runs - you pre-declare what must happen |
| Who fails the test | Your explicit expect(...) afterward |
The mock itself, if the call doesn't match |
| Feel | "Let me check what happened" | "This must happen, or fail" |
In day-to-day practice, with libraries like Jest or Sinon, the same object (jest.fn(), a Sinon spy) plays
both roles - you choose spy-style or mock-style by how you assert. Don't lose sleep over the boundary; the
useful thing is knowing whether you're loosely observing (spy) or strictly demanding (mock).
"But everyone just says 'mock'"
They do, and that's okay. In casual speech, "mock the database" almost always means "put in some double" - usually a stub or a fake, despite the word. The precise vocabulary matters in two moments: choosing a tool ("do I need a stub here, or a fake?" is a real design question), and reading a code review comment ("this is too mock-heavy" is specifically about asserting on interactions, the subject of the next phase). Speak loosely if you like, but think precisely.
Recap
- Dummy - fills a required slot; never actually used.
- Stub - returns canned answers; feeds your code a situation. You assert on your code, not the stub.
- Fake - a real, working, lightweight implementation (in-memory DB); has genuine logic and state.
- Spy - records how it was called so you can inspect the interaction afterward.
- Mock - a stub that also pre-declares how it must be called, and fails the test itself if you don't.
- The real dividing line: dummy/stub/fake provide inputs; spy/mock verify interactions. "Mock" is used loosely for all of them - think precisely even when you speak loosely.
You now have the whole family named. The last phase is the judgment that separates tests that protect you from tests that lie to you.
← Phase 1: Why Fake Anything? · Guide overview · Phase 3: When Mocking Helps vs Hurts →
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 2 questions
1. A stub versus a mock:
2. A 'fake' is...