BXUP Track progress & test yourself — free
QA & Testing · Hands-on lab · ~4 min read
QA & TESTING · HANDS-ON LAB

Lab 04 · AI-Authored Suite Quality Clinic

Take an "AI-generated" test suite that we have seeded with the classic anti-patterns — sleep-based waits, tautological asserts, order dependence, over-mocking — find and fix every one, then run mutation testing to prove your refactor actually strengthened the assertions instead of just tidying the code.

Ananya inherited a repo where a coding agent generated 400 tests in a week. Coverage went up; confidence didn't. This lab is that situation in miniature: a small cart module, a unit suite and an E2E spec that look thorough and pass reliably, but were written the way LLMs actually write tests when nobody reviews them. Research on LLM-generated suites keeps finding the same defect families, so the suite below plants one of each. Mutation testing is your objective referee — before/after mutation scores are the artifact, because "the tests feel better now" is not evidence.

Time, tier & cost

~90 minutes hands-on · Core tier (maps to module M2 and playbooks PB-09/PB-13). Cost: $0 (Jest, Stryker and Playwright are free; the AI reviewer is optional).

Setup

mkdir -p qa-lab-04 && cd qa-lab-04 && npm init -y
npm install --save-dev jest @stryker-mutator/core @stryker-mutator/jest-runner
npm install --save-dev @playwright/test && npx playwright install chromium

cart.js — the module under test (correct code; the defects are in the tests):

function addItem(cart, item, qty) {
  if (qty <= 0) throw new Error("qty must be positive");
  const found = cart.items.find((i) => i.sku === item.sku);
  if (found) found.qty += qty;
  else cart.items.push({ ...item, qty });
  return cart;
}
function cartTotal(cart) {
  const subtotal = cart.items.reduce((s, i) => s + i.price * i.qty, 0);
  const discount = subtotal >= 1000 ? subtotal * 0.1 : 0;
  return Math.round((subtotal - discount) * 100) / 100;
}
function applyCoupon(cart, code) {
  if (code === "SAVE50" && cartTotal(cart) >= 500) cart.coupon = 50;
  else cart.coupon = 0;
  return cartTotal(cart) - cart.coupon;
}
module.exports = { addItem, cartTotal, applyCoupon };

cart.test.js — the "AI-generated" unit suite, verbatim. Do not fix it yet:

const { addItem, cartTotal, applyCoupon } = require("./cart");
let sharedCart = { items: [] };
test("addItem adds an item", () => {
  const result = addItem(sharedCart, { sku: "A1", price: 300 }, 2);
  expect(result).toBe(sharedCart);
  expect(sharedCart.items.length).toBeGreaterThan(0);
});
test("cartTotal returns the total", () => {
  const total = cartTotal(sharedCart);
  expect(total).toEqual(cartTotal(sharedCart));
  expect(typeof total).toBe("number");
});
test("discount applies for big carts", () => {
  addItem(sharedCart, { sku: "B2", price: 400 }, 1);
  const total = cartTotal(sharedCart);
  expect(total).toBeTruthy();
});
test("applyCoupon works", () => {
  const price = applyCoupon(sharedCart, "SAVE50");
  expect(price).toBeLessThanOrEqual(cartTotal(sharedCart));
});
test("rejects bad quantity", () => {
  try {
    addItem({ items: [] }, { sku: "C3", price: 10 }, -1);
  } catch (e) {
    expect(e).toBeDefined();
  }
});

checkout.spec.ts — the "AI-generated" E2E spec (uses any demo page; the anti-patterns are the point, you will not run this one green):

import { test, expect } from "@playwright/test";
test("checkout flow", async ({ page }) => {
  await page.goto("http://localhost:3000");
  await page.waitForTimeout(3000);
  await page.click(".btn-primary");
  await page.waitForTimeout(5000);
  expect(await page.title()).toBeDefined();
});

stryker.conf.json:

{
  "testRunner": "jest",
  "mutate": ["cart.js"],
  "reporters": ["clear-text", "progress"],
  "coverageAnalysis": "perTest"
}

Steps

  1. Get a green, deceptive baseline. Run npx jest. All five tests pass. Record that in clinic-notes.md — a fully green suite is the starting condition, which is exactly why this defect class survives in real repos.
  2. Run the mutation baseline. Run npx stryker run and record the mutation score. Expect it to be dismal — most mutants of cart.js survive because the assertions barely constrain behavior. This number is your "before."
  3. Audit the unit suite against the anti-pattern catalog. Go test by test through cart.test.js and classify every defect you find into: tautological assertion, vacuous assertion, order dependence / shared state, swallowed failure, missing boundary. Write each finding in clinic-notes.md as: test name, line, anti-pattern, why it cannot fail when the code breaks. There are at least six findings; the answer key enumerates them.
  4. Use AI as review partner — with the rubric, not vibes. Paste cart.test.js and cart.js into Claude/ChatGPT: "Review this test suite against this rubric: sleep-based waits, tautological asserts, assertions that can never fail, shared state between tests, exception tests that pass when no exception is thrown, missing boundary values. For each finding cite the line." Compare its findings to yours; log anything it caught that you missed and anything it missed or invented.
  5. Refactor the unit suite. Rewrite cart.test.js: fresh cart per test (no module-level shared state), exact-value assertions (expect(cartTotal(cart)).toBe(540)), expect(() => addItem(...)).toThrow("qty must be positive") for the error path, and explicit boundary tests — subtotal 999 vs 1000 for the discount threshold, coupon at total exactly 500, and the merge-quantity path for a repeated SKU. Every assertion should be able to name the mutant it would kill.
  6. Prove it with mutation testing. Run npx stryker run again. The score should jump substantially (typical honest outcome: from roughly 30–50% to 85%+). For each surviving mutant, decide: add a test to kill it, or write one line in clinic-notes.md on why it is equivalent/not worth killing. No silent survivors.
  7. Fix the E2E spec on paper. Rewrite checkout.spec.ts in your editor: replace both waitForTimeout sleeps with web-first assertions (await expect(page.getByRole(...)).toBeVisible() auto-waits), replace the class selector with a role-based locator, and replace the vacuous title assertion with a real outcome (await expect(page).toHaveURL(/confirmation/) or an on-page assertion). Annotate each change with the anti-pattern it removes.
  8. Write the before/after summary. Close clinic-notes.md with a table: mutation score before/after, count of findings by anti-pattern, which the AI reviewer caught vs missed, and the one-paragraph policy you'd propose for AI-generated tests entering your team's repo.
Pro Tip

Mutation testing is the only honest judge of a refactor like this. Coverage was high before and after; the tests were green before and after. Only the mutation score can tell you whether the assertions now grip the behavior.

Verify

  • clinic-notes.md lists ≥6 unit-suite findings and 3 E2E findings, each with line and anti-pattern class.
  • Before and after Stryker scores recorded; after-score ≥ 85% or every survivor justified in writing.
  • Refactored suite: no shared mutable state, no toBeTruthy/toBeDefined as primary assertions, error path uses toThrow, boundaries 999/1000 and 500 covered.
  • Rewritten E2E spec has zero waitForTimeout calls and at least one real outcome assertion.
  • AI-reviewer comparison logged: caught, missed, invented.

Self-grading rubric (pass = 7/10):

Dimension012
Anti-patterns found≤34–56+ incl. the swallowed exception
Mutation before/afterNot runRun onceBoth runs, survivors dispositioned
Assertion strengthStill genericMostly exactExact values + boundaries + throw
IsolationShared state remainsPartially fixedFresh state every test
E2E hygieneSleeps remainSleeps goneSleeps gone + real outcome asserted
▸Answer key — every seeded anti-pattern and the expected mutation deltasoptional deep dive — click to expand

Unit suite, seeded defects (7):

  1. Shared mutable statemodule-level sharedCart threads through four tests; "discount applies" only crosses the ₹1000 threshold because earlier tests already stuffed the cart. Reorder the tests or run one in isolation and behavior changes. Anti-pattern: order dependence.
  2. expect(result).toBe(sharedCart)asserts the function returns its input reference. True even if addItem does nothing else. Tautological.
  3. expect(total).toEqual(cartTotal(sharedCart))compares the function to itself. The purest tautology in the file; it passes for literally any implementation. This is the "tautological assert" the exam loves.
  4. toBeGreaterThan(0) / toBeTruthy / typeof === "number"vacuous assertions that survive almost any arithmetic mutation (wrong price math, wrong discount rate, >= flipped to > all pass).
  5. The swallowed exceptionthe rejects bad quantity test has no expect.assertions(1) and no toThrow; if addItem stops throwing, the catch never runs and the test still passes. Classic silent pass.
  6. toBeLessThanOrEqual on the coupon — passes whether the coupon applied (₹50 off) or not (equal). Cannot detect a dead applyCoupon.
  7. Missing boundariesnothing tests subtotal 999 vs 1000 (discount threshold, >= vs >), total exactly 500 for SAVE50, or the duplicate-SKU merge branch (found.qty += qty).

E2E spec, seeded defects (3): two waitForTimeout sleeps (flake generator #1 in LLM-authored suites), a styling-class selector (.btn-primary) instead of a role/label locator, and expect(title).toBeDefined() — a page always has a defined title; the test asserts nothing about the checkout.

Expected mutation evidence. Before: Stryker mutants that survive include price * qty → price / qty variants partially caught but subtotal >= 1000 → subtotal > 1000, * 0.1 → * 1/* 0, cartTotal(cart) >= 500 → > 500, found.qty += qty → -= qty, and removal of the throw — typically leaving the score in the 30–50% band. After a correct refactor, every mutant above dies: the 999/1000 pair kills the threshold flips, exact totals kill the arithmetic mutants, toThrow kills the throw-removal, and a duplicate-SKU test kills the merge mutant. Survivors in the after-run should be equivalent-mutant noise only (e.g., rounding-internal rewrites) — each needs a written disposition.

AI-reviewer expectation. Frontier models reliably catch defects 1–3 and the sleeps; they frequently miss #5 (swallowed exception) and #7 (boundaries), and occasionally invent a complaint about Math.round. Your caught/missed/invented log should reflect a split like that — a reviewer that "caught everything" usually means your list was too short.

Key Takeaway:

AI-generated tests fail in predictable families — sleeps, tautologies, shared state, swallowed errors, missing boundaries — and every family is invisible to a green checkmark. Review against the catalog, refactor for exact assertions, and let mutation testing arbitrate: if the mutants live, the tests are decoration.

This is the free reading view. Open BXUP to track your progress, run the hands-on labs, and prove it with original, exam-pattern assessments.
© Anvi Innovate · BXUP — Be Exponentially Upskilled · BXUP is an independent learning platform, not affiliated with or endorsed by AWS, Microsoft, Google, NVIDIA, IBM, Salesforce, Databricks, Oracle or Anthropic. All trademarks belong to their respective owners. · bxup.in