JS Sketchbooksee JavaScript think ✏️
← back to phase 11

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.

watch it happen
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.

three copies, drifting aparttest("coupon SAVE10…") { …the same 5 lines, copy-pasted…test("coupon SAVE25…") { …the same 5 lines, copy-pasted…test("coupon EXPIRED…") { …the same 5 lines, copy-pasted…the smell: three coupons = three nearly-identical copy-pasted tests. Fix a stepin one, forget the other two — drift
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.”

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.

your turn

⌨️ 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. Create cases: the three rows — SAVE10 → 900, SAVE25 → 750, EXPIRED → 1000 (objects with code and total).
  • Write makeTests(cases): for each row, produce { name, run } — the name built from the data as coupon CODE → TOTAL (template literal), the run a function returning whether applyCoupon(1000, code) strictly equals the row’s total.
  • Run them all: print ✓ name per passing test, then the summary 3 passed — counted, not typed.

when you press RUN, the console must show exactly:

✓ coupon SAVE10 → 900
✓ coupon SAVE25 → 750
✓ coupon EXPIRED → 1000
3 passed

✏️ 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?

teach it back

🗣️ 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.

a few sentences, minimum — you’ve got this
to remember
Copy-paste tests drift (regression inside the suite). Data-driven: a TABLE of cases + one mold; for...of at COLLECTION time manufactures a real test per row — independently reported/retried/traced.
Names from data (sentence rule scales) · edges cost one row each (cheap questions get asked) · boundary: a table varies inputs of ONE behavior, never mixes behaviors.
Created things need unique-per-run identities: ada+${Date.now()}@… (+suffix trick). Identity = run-unique; expectations = deterministic. Bigger tables live in JSON files (9.5 + 4.13); small config in env (9.4).
next: 11.9 Page Object Model →