JS Sketchbooksee JavaScript think ✏️
← back to phase 10

10.6 — Test doubles & a taste of TDD

A unit test must live in 10.2’s fast lane — no network, no disk, no clock. But real machines DEPEND on those things. The trade’s answer: test doubles — stand-ins for a dependency, like a stunt double for an actor. Today you’ll meet the whole family precisely (stub, spy, mock, fake), build two of them from raw materials, and take one honest lap of the most famous rhythm in the craft: red–green–refactor.

watch it happen
// the machine depends on the NETWORK:
async function greetUser(fetchUser, id) {
  const user = await fetchUser(id);
  return `hello, ${user.name}!`;
}

// a STUB — canned answer, no network:
const stubFetch = async (id) => ({ name: "Ada" });

// a SPY — records every call (a 5.3 closure!):
function makeSpy(fn) {
  const calls = [];
  const wrapped = (...args) => {
    calls.push(args);
    return fn(...args);
  };
  wrapped.calls = calls;
  return wrapped;
}

const spy = makeSpy(stubFetch);
await greetUser(spy, 7);
// spy.calls → [[7]]

The machine under test: greetUser fetches a user, then formats a greeting. Unit-test it as-is and you drag in 6.7’s ENTIRE round trip: slow (hundreds of ms), flaky (networks fail randomly), and it needs a server running with user 7 in it. Three violations of the fast lane in one dependency.

the machine and its dependency socketgreetUserthe machine under testthe REAL networkslow · flaky · needs a server🌐 6.7’s whole round tripunit-testing greetUser as-is drags in the REAL network: slow, flaky, needs aserver running
under the hood

The deeper story, with the real names for things — this part is what turns “I saw it” into “I can explain it.”

Real frameworks ship the spy factory you just hand-built: Vitest’s vi.fn() creates a spy (its notebook is .mock.calls), and vi.spyOn(object, "method") wraps an existing method in place. Read their docs after building makeSpy and you’ll recognize every feature — a closure, a notebook, a delegate.

The clock is the sneakiest boundary: code using Date.now() or setTimeout gives different answers every run — untestable by definition until you double TIME itself. vi.useFakeTimers() swaps the clock for a puppet you advance by hand (vi.advanceTimersByTime(2000) — two “seconds,” instantly). Phase 11 has a browser-side sibling, page.clock.

The honest boundaries of TDD: it shines when behavior is clear and design is the question (parsers, calculators, your tip functions) — it’s clumsy for exploratory UI work where you don’t yet know what you’re building. Most professionals use it selectively, not religiously. What survives everywhere is the RED discipline: never trust a test you’ve never seen fail.

Job note: Playwright’s route.fulfill() (11.10 in your map) is a STUB — for the network, at the browser boundary: “when the page asks /api/users, answer THIS.” Same family, biggest stage. And “explain stub vs mock vs spy” is a genuine interview staple — you now hold the precise answer most candidates fumble.

your turn

⌨️ build a spy, deploy a stub

The two working doubles, from raw materials: a stub that feeds greetUser a canned user, and a spy — a closure with a notebook — that proves exactly how the dependency was called.

requirements:

  • Write greetUser(fetchUser, id): an async function that awaits fetchUser(id) and returns hello, NAME! built from the user’s name (template literal).
  • The stub: stubFetch — an async arrow that ignores the id and resolves to { name: "Ada" }.
  • The spy: makeSpy(fn) — inside, a calls array (the closure’s notebook, 5.3); return a wrapped function that gathers its arguments (rest, 4.11), pushes them into calls, and delegates to fn. Attach the notebook: wrapped.calls = calls.
  • Run the sting inside an async function: wrap the stub in a spy, await greetUser(spy, 7) and print the greeting, then print calls: 1 (from the notebook’s length) and first arg: 7 (from the recorded arguments).

when you press RUN, the console must show exactly:

hello, Ada!
calls: 1
first arg: 7

✏️ Quick check 1

One double FEEDS canned answers in; another RECORDS how it was called. Name them in that order (comma-separated).

✏️ Quick check 2

In TDD, why must you watch the test FAIL before writing the code? (what does the red prove?)

✏️ Quick check 3

Your test stubs YOUR OWN withTax function to test YOUR OWN cart. Good idea or bad idea?

teach it back

🗣️ Now teach it back

Teach the double family to a friend: the problem doubles solve, each member’s precise job (stub/spy/mock/fake — with your 9.5 example), the boundaries rule — then walk one red-green-refactor lap and say what each beat proves.

Write it as if your friend is sitting next to you. Saved to your journal — future-you will use these notes to teach others.

a few sentences, minimum — you’ve got this
to remember
Doubles = stand-ins for dependencies, made possible by injection (3.8: hand the dependency in, and you can swap it). Double at BOUNDARIES only — network, disk, clock, randomness; never what you own.
The family, precisely: STUB feeds canned answers (input side) · SPY records calls (output side; a 5.3 closure with a notebook — you built one) · MOCK = stub+spy+expectations (the loosely-used word) · FAKE = working lightweight stand-in (9.5’s model disk!).
TDD’s lap: RED (test first — the failure PROVES the test can fail) → GREEN (minimum code to pass) → REFACTOR (improve under a green safety net). Never trust a test you’ve never seen fail.