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.
// 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 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.
⌨️ 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 awaitsfetchUser(id)and returnshello, 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, acallsarray (the closure’s notebook, 5.3); return a wrapped function that gathers its arguments (rest, 4.11), pushes them intocalls, and delegates tofn. 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 printcalls: 1(from the notebook’s length) andfirst arg: 7(from the recorded arguments).
when you press RUN, the console must show exactly:
✏️ 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?
🗣️ 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.