Why Fake Anything?
Here's a function that looks completely reasonable until you try to test it:
The logic you wrote is small and worth testing: reject missing customers, reject non-positive amounts,
record the payment after a successful charge. But to run a single test of that logic, the function drags in
a live database (db) and a real payment provider (stripe). To test "does it reject a negative amount?"
you'd need a working Stripe account and a real customer in a real database. That's the problem test doubles
exist to solve.
The dependency is not the thing you're testing
What's actually going on. Your function has two kinds of parts:
- The logic you own - the
ifchecks, the order of operations, what you do with the result. This is what the test should be about. - The dependencies it calls - the database, the payment API, the file system, the clock. These belong to someone else (a library, a service, the operating system). They're already tested by their authors. You're not trying to test them; you're trying to test how your code uses them.
A test double lets you swap out that second category so only the first category is under the microscope.
Without doubles With doubles
────────────── ────────────
┌──────────────┐ ┌──────────────┐
│ your function│ │ your function│ ← the only thing
└──────┬───────┘ └──────┬───────┘ under test
│ calls │ calls
▼ ▼
┌──────────────┐ ┌──────────────┐
│ REAL Stripe │ slow, costs money │ FAKE Stripe │ ← a stand-in you
│ REAL database│ needs a server │ FAKE database│ control completely
└──────────────┘ └──────────────┘
📝 Terminology - "the system under test" (SUT). The bit of code a given test is actually checking. Everything else the test touches is scaffolding. Test doubles are scaffolding: they exist so the SUT can run, not because we care what they do.
The four reasons a real dependency hurts in a test
You don't fake things for fun - faking adds its own cost (the double can drift from reality, which is the whole subject of Phase 3). You fake a dependency when keeping it real would make the test one of these:
| The dependency is… | Why it ruins the test | Example |
|---|---|---|
| Slow | A test suite that takes minutes stops getting run | A real HTTP call, a full database round-trip |
| Unreliable / nondeterministic | The test passes Monday, fails Tuesday, for no code reason | A third-party API that's down; new Date(); a random number |
| Expensive or irreversible | The test has real-world side effects you can't take back | Charging a card, sending an email, deleting a row in prod |
| Hard to set up | You can't easily force the exact situation you want to test | Making the network fail on demand to test your retry logic |
That last row is the one people forget. Doubles aren't only about avoiding the real thing - they're often the only practical way to test the unhappy paths. You can't reliably make a real API time out, but a double can be told to throw a timeout every single time, on command.
⚠️ Gotcha - the clock is a dependency too. Code that calls new Date(), time.Now(), or
System.currentTimeMillis() directly is silently depending on when the test runs. A test like "this
coupon is expired" will pass today and fail after the expiry date - a bug that travels into the future.
Time is one of the most common things worth faking, and one of the easiest to overlook because it doesn't
look like a dependency.
The stunt-double mental model
This is where the name comes from, and it's the picture worth keeping.
In a film, the lead actor does the close-up dialogue - the part the movie is actually about. But when the scene calls for jumping off a building, a stunt double stands in, doing the dangerous part safely so the actor (and the production budget) stay intact.
A test double is exactly that for your code:
- The real dependency is the dangerous, expensive, or unavailable performer.
- The double is a stand-in that looks enough like it (same shape, same method names) for this test.
- Your code under test never knows the difference - it calls
stripe.charge(...)the same way regardless.
The reason this works at all is that your code talks to the dependency through some interface - a set
of method names and shapes, like "an object with a charge(token, amount) method that returns a promise
of { id }." As long as the double honors that shape, your code is satisfied. The double doesn't have to
be Stripe; it only has to look like Stripe from where your function is standing.
┌─────────────────────┐
│ your function │
└──────────┬──────────┘
│ "I need something with a
│ .charge(token, amount) method"
▼
┌──────────────┐
│ interface │ ← the shape your code depends on
└──────┬───────┘
┌────────┴────────┐
▼ ▼
┌────────────┐ ┌────────────┐
│ REAL Stripe│ │ DOUBLE │ ← either one satisfies the shape;
└────────────┘ └────────────┘ in a test you plug in the double
💡 Key point. Faking is only possible because your code depends on a shape, not a specific object.
This is why "depend on interfaces, pass dependencies in" (rather than reaching out and grabbing a global
stripe from inside the function) makes code testable - it gives you the seam to slide a double into.
Hard-to-test code is usually code with no seam.
So what is a double, concretely?
It's just an object (or function) you create in the test that stands in for the real one. The simplest possible version is hand-written - no library involved:
// A hand-rolled stand-in for the database, created inside the test.
const = ;
// A hand-rolled stand-in for Stripe.
const = ;
// Now we can test our logic with no network and no real database.
const = await ;
What just happened: We built two plain objects with the same method names the real dependencies have, and
passed them into the function. chargeCustomer calls db.findCustomer(...) and stripe.charge(...) exactly
as before, but those calls now hit our stand-ins, which return instantly and cost nothing. The test can run
on a plane with no Wi-Fi.
Mocking libraries exist to make this less tedious - generating these stand-ins, recording how they were called, asserting on it. But there is nothing magic underneath: a double is a stand-in object that honors the shape your code expects. Everything in the next phase varies only how much the stand-in does.
Recap
- Your test should be about the logic you own, not the dependencies it calls - those belong to someone else and are already tested.
- You replace a real dependency with a double when keeping it real would make the test slow, unreliable, expensive/irreversible, or impossible to set up (especially the unhappy paths).
- The clock and randomness are dependencies too, even though they don't look like it.
- Faking works because your code depends on a shape (an interface), not a specific object - a double just honors that shape. Code with no seam to inject a double is the hard-to-test code.
Now that you know why we fake, the next phase names the family - "a double that returns canned answers" and "a double that asserts it was called correctly" are different tools, even though people call them all "mocks."
← Guide overview · Phase 2: The Doubles, Clearly Defined →
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. You replace a real dependency with a test double when keeping it real would make the test...
2. Which is also a dependency worth faking, even though it doesn't look like one?