When Mocking Helps vs Hurts
Here's the uncomfortable truth nobody tells you when they hand you a mocking library: a test full of mocks can pass while your product is on fire. Doubles let you replace reality, and the more of reality you replace, the less your test is actually checking. Used at the right seam, doubles are essential. Used everywhere, they produce a suite that's green, fast, and worthless.
This phase is the judgment - and it comes down to one question and a couple of habits.
The one question: is this the boundary, or my own code?
Picture your application as a region with a border. Inside the border is code you own - your functions calling your other functions, your objects, your business logic. At the border are the external systems you don't own - the database, third-party APIs, the network, the file system, the clock, the payment provider.
┌─────────────── your application ───────────────┐
│ │
│ service ──► validator ──► calculator │ ← your own code:
│ │ │ use the REAL objects
│ │ │
└──────┼──────────────── boundary ──────────────-┘
│
▼
┌─────────────┐ ┌─────────────┐ ┌────────────┐
│ database │ │ payment API │ │ the clock │ ← external systems:
└─────────────┘ └─────────────┘ └────────────┘ fake THESE
💡 Key point - mock at the boundary, not inside it. Replace the things at the border (slow, external,
nondeterministic - exactly the four reasons from Phase 1). Use the real thing
for your own code. When service calls validator, let it call the real validator - that interaction is
part of what you're trying to verify, not noise to be stubbed away.
This single rule prevents most mocking pain. Everything below is why it works.
The over-mocking trap, made concrete
When you mock your own internals, two specific failures appear. They're worth seeing clearly because they're sneaky - the tests look fine.
Trap 1: green tests over a broken product
Suppose calculateTotal has a real bug - it forgets to add tax. Now look at a test of the function that
calls it:
// We mocked our OWN calculator instead of using the real one.
const = ;
const = await ;
.; // passes! ...but the real calculator returns 100.0
What just happened: We told the mock to return 108.0 - the answer we believe is correct - so the test
passes. But the real calculateTotal is broken and returns 100.0. Our test never ran the real code, so
it can't see the bug: we've encoded our assumption and verified it against itself. The test is green; the
customer gets undercharged.
⚠️ Gotcha - a mock freezes your belief about a dependency in time. The mock returns what you thought the dependency does on the day you wrote the test. If the real dependency changes (or was wrong all along), the mock keeps cheerfully returning the old answer, and the test keeps passing while production breaks. Real objects can't lie to you like this; they run the actual code.
Trap 2: tests welded to implementation details
Over-mocking pushes you to assert on how your code works internally instead of what it produces:
// Asserting on internal call sequence - coupled to the implementation, not the behavior.
.;
.;
.;
What just happened: These assertions don't check that the order was saved correctly - they check the
exact sequence of internal calls the code happens to make today. The moment someone refactors save to do
the same thing a slightly different way (say, a batch insert), every one of these tests breaks, even though
the behavior is identical and correct.
This is the quiet tax of over-mocking: tests that break when you refactor working code and stay green when you break working code - exactly backwards from what a test is for. A test should pin down behavior and stay silent about implementation, so you can refactor freely.
🪖 War story. A team I know had a 4,000-test suite that ran in 90 seconds and was almost entirely mocks. It went green on every commit. Then a real outage: a downstream API had changed a field name months earlier and not one test caught it - every test touching that API used a mock returning the old shape. The suite wasn't testing the software; it was testing a museum replica of it as it existed the day each mock was written.
Prefer real objects and fakes when they're cheap
The instinct to reach for a mock is often premature. Walk down this ladder and stop at the first rung that works:
| Reach for… | When | Why it's better |
|---|---|---|
| The real object | It's your own code, or a fast pure dependency | Tests the actual code; survives refactors |
| A fake (in-memory) | The dependency has real behavior (a DB, a cache) | Real logic + state, still fast and deterministic |
| A stub | You just need to feed one canned situation | Simple; forces a specific input |
| A mock | The interaction itself is the contract you must guarantee | Verifies the call was made correctly |
The order matters. A real object is the most trustworthy test you can write, so prefer it. Drop to a fake when the real thing is too slow or external but still has behavior worth honoring (an in-memory database is the classic - see Phase 2). Drop to a stub when you only need to set up a situation. Only reach for a strict mock when the interaction is genuinely the point - "we must call the payment gateway exactly once, never twice." Double-charging is a real bug a mock catches well.
Where doubles thin out: the testing pyramid
This connects to a bigger picture. Different layers of tests use different amounts of faking, on purpose:
- Unit tests sit at the bottom: small, fast, lots of doubles at the boundary so a single piece of logic can be checked in isolation.
- Integration tests sit above: they wire several real pieces together - often including a real (or fake) database - and use far fewer doubles, precisely so they catch the "the parts don't actually fit together" bugs an over-mocked unit test sails right past.
- End-to-end tests sit at the top: ideally almost no doubles - the real system, exercised like a user would.
The mocked-unit-test and the no-mocks-integration-test aren't competitors; they're checking different things. The bug in Trap 1 (real calculator returns the wrong number) is exactly what an integration test with a real calculator would catch - why you don't rely on heavily-mocked unit tests alone.
⏭️ For how these layers fit together - what each one is for, how many of each to write, and why the shape matters - see Unit, Integration & E2E Tests. The one-line version: the further up the pyramid, the fewer doubles, because the whole point of the higher layers is to test the real seams that doubles hide.
A short checklist before you mock
Before you replace something with a double, ask:
- Is it at the boundary? (external, slow, nondeterministic, or has irreversible side effects) → a double is appropriate. Is it my own code? → strongly prefer the real object.
- Could a real object or a fake do this cheaply? → use that instead of a mock.
- Am I about to assert on how the code works rather than what it produces? → stop; that test will break on the next harmless refactor.
- If the real dependency changed shape tomorrow, would any test catch it? → if every test of it is mocked, the answer is no. Make sure something (an integration test) exercises the real seam.
Recap
- Mock at the boundary, not inside it. Fake external systems; use real objects for your own code.
- Over-mocking produces lying tests - green over a broken product (because the mock encodes your assumption) and brittle against refactors (because it's welded to implementation details).
- A mock freezes your belief about a dependency; if reality drifts, the mock keeps the test green while production breaks.
- Walk the ladder: real object → fake → stub → mock. Stop at the first rung that works; only mock when the interaction is the contract.
- Doubles thin out as you go up the pyramid - integration and E2E tests use fewer of them on purpose, to catch exactly the bugs heavily-mocked unit tests miss. See Unit, Integration & E2E Tests.
That's the whole craft: doubles are a scalpel for isolating your logic, not a way to make every test fast and green. Use them at the seams, keep your own code real, and let the higher layers test what the doubles hid.
← Phase 2: The Doubles, Clearly Defined · Guide overview
Related guides: Your First Unit Test · Unit, Integration & E2E Tests
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. The rule that prevents most mocking pain is...
2. Over-mocking produces lying tests because...