A coding agent can take a module from 0% to 90% coverage before lunch — and every one of those tests can be worthless. Coverage measures execution, not detection. The quality gate for AI-generated tests is mutation testing: break the code on purpose and count how many tests notice.
Tier: Core · Workflow area: Automation engineering
This playbook pairs the fastest use of coding agents — backfilling tests for untested code — with the verification that makes the output trustworthy. Generated coverage without assertion strength is a liability that looks like an asset, which is the most dangerous kind.
When NOT to use:
mutmut for Python, Stryker for JS/TS, PIT for JVM — and runnable on the target module in minutes, not hours.Pick the target and capture current behavior intent. Tell the agent what the module is for — without intent, it writes tautologies that enshrine current behavior, bugs included:
Generate pytest tests for src/billing/proration.py (currently untested).
Purpose: computes mid-cycle subscription charges; must round half-up to
cents; must never produce negative charges; upgrades take effect
immediately, downgrades at period end.
Requirements:
- Test observable behavior via public functions only.
- Include boundary cases: first/last second of period, zero-day
remainder, leap day, plan price of 0.
- Mark anything that looks like a BUG in the current code with a
"# SUSPECTED BUG" comment and test the *correct* behavior as xfail —
do not lock in the wrong behavior silently.
- No mocking of pure functions. Use freezegun for time.
Run: pytest tests/billing/test_proration.py -q — iterate until green.
Review the suspected-bug flags first. This is the highest-value output of the whole generation: places where written intent disagrees with the code. Each one goes to a human decision — real bug (file it, keep the xfail) or wrong intent (fix the test).
Check coverage — as a floor, not a verdict:
pytest tests/billing/ --cov=src/billing --cov-report=term-missing -q
Uncovered lines are work remaining. Covered lines prove nothing yet.
Run the mutation gate. Python — mutmut; JS/TS — Stryker:
mutmut run --paths-to-mutate src/billing/proration.py
mutmut results
npx stryker run --mutate "src/billing/**/*.ts"
Mutation testing changes >= to >, deletes statements, flips constants — each surviving mutant is a code change your suite cannot detect. Expect agent-generated suites to land far below their coverage number on first run; that gap is the finding.
Feed survivors back to the agent — this loop is the playbook:
mutmut reports these surviving mutants in proration.py: [paste
`mutmut show` output for each]. For each survivor, either strengthen an
existing test or add one that kills it. Do NOT write assertions that
merely restate the mutated line; assert the behavioral consequence.
Rerun pytest and tell me which mutants you expect each change to kill.
Re-run mutmut. Two or three iterations typically move a suite from ~55% to 85%+ mutation score. Survivors that remain should be explainable — equivalent mutants or accepted risk, listed in the PR description.
Human-review the final suite against the pb-13 rubric — especially over-mocking and tests that duplicate each other's kills — then merge with both numbers in the PR: coverage and mutation score.
A test suite's job is to fail; coverage says the code ran, only mutation says the suite would have caught a change — always report the two numbers together or the cheap one will be mistaken for the real one.
stryker.config.json with "testRunner": "vitest" and use --incremental so iteration runs stay fast on big modules.Let the agent write the tests and let mutation testing referee them — the generated suite you can trust is the one that earned its score against deliberately broken code, not the one with the prettiest coverage badge.