11.8 — Test data & parameterized tests
Three coupon codes to test. The beginner writes three nearly identical tests; six months later they’ve drifted apart and one is lying. The professional writes ONE test body and a TABLE of cases — a loop that manufactures a real, independent test per row. Data-driven testing: how suites scale without rotting, plus the unique-data trick that keeps created things from colliding.
const cases = [
{ code: "SAVE10", total: 900 },
{ code: "SAVE25", total: 750 },
{ code: "EXPIRED", total: 1000 }, // edge: 1 line
];
for (const c of cases) {
test(`coupon ${c.code} → ${c.total}`,
async ({ page }) => {
await page.goto("/checkout");
await page.getByLabel("Coupon").fill(c.code);
await page.getByRole("button", { name: "Apply" })
.click();
await expect(page.getByTestId("total"))
.toHaveText(String(c.total));
});
}
// unique data per run — no collisions:
const email = `ada+${Date.now()}@shop.com`;The smell first: three coupons, three copy-pasted tests, five identical lines each. Now the checkout flow gains a step — you fix two copies and forget the third. Copy-paste drift is 10.1’s regression story happening INSIDE the suite: the guards themselves rot.
The deeper story, with the real names for things — this part is what turns “I saw it” into “I can explain it.”
Why generation-at-collection works: the spec file is a MODULE (8.1) that the runner imports before running anything. Top-level code — including your for...of — executes during that import, and each test(…) call registers a definition. By the time browsers launch, the suite is a fixed list. (This also means the table must be available synchronously at load — reading it from a file is fine; fetching it from a server mid-collection is not.)
For truly unique data under parallel workers, timestamps can theoretically collide (two workers, same millisecond). Real suites often add the worker index (testInfo.workerIndex) or a random suffix. The principle stays: identity = run-unique; expectations = deterministic.
You may meet “property-based testing” in reading — generating RANDOM inputs to hunt edge cases. Powerful for pure functions; used sparingly in E2E where determinism is king. Know the term; reach for tables first.
Job note: the interview question here is “how would you test 50 coupon codes?” — the answer that signals experience is the table (with named rows and a one-line edge policy), NOT “I’d write 50 tests” and NOT “I’d loop inside one test.” You now know why both wrong answers are wrong, which is worth more than the right one.
⌨️ build the test generator
The starter ships the machine under test. Build the data-driven suite: a table of cases, a generator that manufactures one named test per row, and a runner that reports each independently (10.5’s runner, meeting 11.8’s table).
requirements:
- Keep the starter’s
applyCoupon. Createcases: the three rows —SAVE10 → 900,SAVE25 → 750,EXPIRED → 1000(objects withcodeandtotal). - Write
makeTests(cases): for each row, produce{ name, run }— the name built from the data ascoupon CODE → TOTAL(template literal), the run a function returning whetherapplyCoupon(1000, code)strictly equals the row’s total. - Run them all: print
✓ nameper passing test, then the summary3 passed— counted, not typed.
when you press RUN, the console must show exactly:
✏️ Quick check 1
A for...of over 3 cases calls test() each lap. How many tests does the runner see — one or three?
✏️ Quick check 2
A test registers a new user with a fixed email. It passed yesterday. What happens on today’s run?
✏️ Quick check 3
One table holds rows for “apply coupon”, “delete account”, and “check footer”. What rule does it break?
🗣️ Now teach it back
Explain data-driven testing to a friend: the copy-paste smell, how the generation loop actually works (when it runs, what the runner sees), what happens to names and edges, the one-behavior boundary, and the unique-data trick.
Write it as if your friend is sitting next to you. Saved to your journal — future-you will use these notes to teach others.