# Mocking, Stubbing & Test Doubles

> What test doubles actually are, the difference between a dummy, stub, fake, spy, and mock, and the judgment call of when to fake a dependency versus use the real thing.


---

# Mocking, Stubbing & Test Doubles

You wrote a function. It does one small, sensible thing. But to *run* it in a test, it insists on calling
a payment API, or reading from a database, or checking what time it is. Suddenly your tiny unit test needs
a network connection, a live database, and a credit card. The thing you actually want to test is buried
under a pile of dependencies you don't control.

This guide is about the escape hatch: **test doubles** - stand-ins you swap in for the awkward, slow, or
expensive parts so you can test *your* logic in isolation. You'll learn what each kind of double actually
is (the words "mock," "stub," and "fake" get thrown around interchangeably, and we'll untangle that), and
the harder part - knowing when faking *helps* and when it quietly *hurts*.

## How to read this

- **Just need to know mock vs stub vs fake right now?** Jump to [Phase 2: The Doubles, Clearly Defined](02-the-doubles-defined.md) - it's a labeled tour of the whole family.
- **Want it to finally make sense?** Read in order. Phase 1 builds the mental model (why we fake anything at all), Phase 2 names the tools, and Phase 3 gives you the judgment to use them well.

## The phases

1. **[Why Fake Anything?](01-why-fake-anything.md)** - the core problem: your code talks to slow, unreliable, or expensive things, and to test your own logic you replace them with stand-ins. The stunt-double mental model.
2. **[The Doubles, Clearly Defined](02-the-doubles-defined.md)** - the family with the confusion cleared up: dummy, stub, fake, spy, and mock. What each one is *for*, with a small example each.
3. **[When Mocking Helps vs Hurts](03-when-mocking-helps-vs-hurts.md)** - the judgment call: mock at the boundaries, not your own internals; the over-mocking trap (green tests over a broken product); when a real object or a fake beats a mock.

> Test-double *libraries* (Jest's `jest.fn()`, Python's `unittest.mock`, Mockito, Sinon) all have their
> own syntax, and we won't try to be a reference for any one of them. This guide teaches the concepts that
> sit underneath every one of them - once you understand those, the library docs read easily.


---

# Why Fake Anything?

Here's a function that looks completely reasonable until you try to test it:

```javascript
async function chargeCustomer(customerId, amount) {
  const customer = await db.findCustomer(customerId);
  if (!customer) throw new Error("No such customer");
  if (amount <= 0) throw new Error("Amount must be positive");

  const result = await stripe.charge(customer.cardToken, amount);
  await db.recordPayment(customerId, amount, result.id);
  return result.id;
}
```

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 `if` checks, 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.

```text
   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](03-when-mocking-helps-vs-hurts.md)). 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.

```text
        ┌─────────────────────┐
        │   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:

```javascript
// A hand-rolled stand-in for the database, created inside the test.
const fakeDb = {
  findCustomer: async (id) => ({ id, cardToken: "tok_test" }),
  recordPayment: async () => {},
};

// A hand-rolled stand-in for Stripe.
const fakeStripe = {
  charge: async () => ({ id: "ch_123" }),
};

// Now we can test our logic with no network and no real database.
const id = await chargeCustomer("cust_1", 50, { db: fakeDb, stripe: fakeStripe });
```

*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

1. Your test should be about **the logic you own**, not the dependencies it calls - those belong to someone
   else and are already tested.
2. 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).
3. The **clock and randomness are dependencies too**, even though they don't look like it.
4. 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."


---

# 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**:

```text
   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.**
```javascript
// createOrder needs a `logger`, but the "rejects empty cart" path
// never logs anything - so any object will do.
const dummyLogger = {};

expect(() => createOrder([], dummyLogger)).toThrow("Cart is empty");
```
*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.**
```javascript
// 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 orderRepoStub = {
  findByUser: async () => [],
};

const summary = await buildDashboard("user_42", orderRepoStub);

expect(summary.message).toBe("You have no orders yet.");
```
*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:
```javascript
const flakyApiStub = {
  fetchRates: async () => { throw new Error("503 Service Unavailable"); },
};
// 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.**
```javascript
// A fake user repository: a real, working store backed by a Map.
function makeFakeUserRepo() {
  const users = new Map();
  return {
    save: async (user) => { users.set(user.id, user); },
    findById: async (id) => users.get(id) ?? null,
  };
}

const repo = makeFakeUserRepo();
await registerUser({ id: "u1", name: "Ada" }, repo);

// It behaves like the real thing: what we saved, we can read back.
expect(await repo.findById("u1")).toEqual({ id: "u1", name: "Ada" });
```
*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.**
```javascript
// A spy email sender: it records every call instead of sending.
const sentEmails = [];
const emailSpy = {
  send: (to, subject) => { sentEmails.push({ to, subject }); },
};

await registerUser({ id: "u1", email: "ada@example.com" }, repo, emailSpy);

// Now we inspect the logbook AFTER running our code.
expect(sentEmails).toHaveLength(1);
expect(sentEmails[0].to).toBe("ada@example.com");
```
*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):
```javascript
const gateway = { charge: jest.fn().mockResolvedValue({ id: "ch_1" }) };

await checkout(cart, gateway);

// The assertion IS about the interaction with the double.
expect(gateway.charge).toHaveBeenCalledTimes(1);
expect(gateway.charge).toHaveBeenCalledWith("tok_visa", 4999);
```
*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

1. **Dummy** - fills a required slot; never actually used.
2. **Stub** - returns canned answers; feeds your code a situation. You assert on *your code*, not the stub.
3. **Fake** - a real, working, lightweight implementation (in-memory DB); has genuine logic and state.
4. **Spy** - records how it was called so you can inspect the interaction *afterward*.
5. **Mock** - a stub that *also* pre-declares how it must be called, and fails the test itself if you don't.
6. 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.


---

# 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.

```text
        ┌─────────────── 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](01-why-fake-anything.md)). 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:

```javascript
// We mocked our OWN calculator instead of using the real one.
const calculatorMock = {
  calculateTotal: jest.fn().mockReturnValue(108.0), // we hand-typed the "right" answer
};

const receipt = await buildReceipt(cart, calculatorMock);

expect(receipt.total).toBe(108.0); // 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:

```javascript
// Asserting on internal call sequence - coupled to the implementation, not the behavior.
expect(repo.beginTransaction).toHaveBeenCalled();
expect(repo.insertRow).toHaveBeenCalledBefore(repo.commit);
expect(repo.commit).toHaveBeenCalledTimes(1);
```
*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](02-the-doubles-defined.md)). 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](/guides/unit-integration-e2e). 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:

1. **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.
2. **Could a real object or a fake do this cheaply?** → use that instead of a mock.
3. **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.
4. **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

1. **Mock at the boundary, not inside it.** Fake external systems; use real objects for your own code.
2. **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).
3. A mock **freezes your belief** about a dependency; if reality drifts, the mock keeps the test green while
   production breaks.
4. **Walk the ladder:** real object → fake → stub → mock. Stop at the first rung that works; only mock when
   the *interaction* is the contract.
5. **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](/guides/unit-integration-e2e).

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.

**Related guides:** [Your First Unit Test](/guides/your-first-unit-test) · [Unit, Integration & E2E Tests](/guides/unit-integration-e2e)
