BXUP Track progress & test yourself — free
Software Engineering · Playbook · Foundation · ~4 min read
SOFTWARE ENGINEERING · PLAYBOOK · FOUNDATION

Coverage Lift on a Legacy Module — With a Mutation-Testing Quality Gate

AI can take a legacy module from 20% to 80% coverage in an afternoon — and most of those generated tests will be worthless assertions like assert result is not None. Coverage percentage measures lines executed, not bugs caught.

Tier: Foundation · Workflow area: Testing & Quality

This playbook pairs AI test generation with mutation testing, so the metric you report is "mutants killed," not "lines touched." That is the difference between a suite that protects a refactor and one that decorates a dashboard.

When to use

  • A module you must change soon has thin or no tests, and you refuse to refactor blind.
  • A coverage mandate landed on your team and you want it to mean something.
  • You inherited code whose original authors are gone and the tests are the only spec you will ever get.

Skip this for code scheduled for deletion, or for modules where integration tests already exercise the real risk (I/O boundaries, third-party calls) — unit-coverage theater there wastes the mutation budget.

Prerequisites

  • A coding agent or assistant with repo access (Copilot agent mode, Claude Code, Cursor, or Continue.dev).
  • A coverage tool wired into your test runner (coverage.py, nyc, JaCoCo).
  • A mutation tester for your stack: mutmut or cosmic-ray (Python), Stryker (JS/TS/C#), Pitest (JVM). All have free tiers or are OSS.
  • A green baseline: the existing suite passes before you add anything.

The workflow

  1. Map the risk, not the gaps. Run coverage and ask the assistant: Here is the coverage report and the module source. Rank the uncovered functions by blast radius: which are called from the most places, mutate state, or handle money/auth/dates? Give me a top-5 target list with one sentence of risk each. You test the dangerous 5, not the alphabetical 40.

  2. Generate behavior-first tests per target. For each target function, prompt:

    Write pytest tests for `apply_late_fee()` below. Rules:
    - One behavior per test, named test_<behavior>, e.g. test_waives_fee_within_grace_period.
    - Assert on concrete output values and state changes, never just "is not None" or "no exception".
    - Cover: the happy path, each branch, boundary values (day 0, day 30, day 31 of grace),
      and the documented error path (negative balance raises ValueError).
    - Use the existing fixtures in tests/conftest.py; do not invent new database mocks.
    
  3. Curate before you commit. Delete tests that restate the implementation (mocking the internals and asserting the mock was called), tests asserting incidental behavior (dict ordering, exact log strings), and duplicates. Expect to cut 20–40% of what was generated. A test you would not want to maintain is a liability, not coverage.

  4. Run the mutation gate. mutmut run --paths-to-mutate src/billing/fees.py. Every surviving mutant is a concrete gap: the mutation tester changed >= to > or deleted a line, and your suite stayed green.

  5. Close survivors with targeted prompts. Paste survivors back: Mutant 14 survived: line 52, days >= gracebecamedays > grace. Write the single test that kills exactly this mutant. Repeat until the mutation score on the target functions clears your threshold (70%+ is a strong bar for legacy code).

  6. Commit with the real metric. PR description states both numbers: "Coverage 22% → 81%; mutation score on changed files 76% (mutmut report attached)."

Key Takeaway:

Coverage says your tests ran the code; mutation score says your tests would notice if the code were wrong. Only the second one justifies trusting the suite during a refactor.

Tool variants

  • Copilot agent mode / Claude Code: can run the coverage-and-mutate loop themselves — instruct them to iterate until the mutation threshold passes, then review the final suite yourself.
  • Cursor / Windsurf: generate per-file with the target function pinned in context; run mutmut/Stryker in the terminal yourself.
  • Continue.dev + local model: workable for step 2 on well-factored functions; keep the risk-ranking (step 1) on a stronger hosted model if policy allows.

Verification

  • Mutation score on the targeted functions meets your threshold — not just line coverage.
  • Every committed test fails if you manually revert its target behavior (spot-check two by breaking the code on purpose).
  • No test in the final suite mocks the unit under test's own internals.
  • Suite runtime grew acceptably; generated tests did not add sleeps or real network calls.

Pitfalls

  • Reporting coverage as if it were quality. 80% coverage with a 30% mutation score is worse than honest ignorance — it invites a confident refactor into a minefield.
  • Letting the AI lock in current bugs. Generated tests assert what the code does, including its defects. When a generated assertion looks wrong, check the spec or a domain owner before enshrining it.
  • Mutation-testing the whole repo on day one. Mutmut on a large codebase runs for hours. Scope to the files you changed.
  • Accepting mock-heavy tests for pure logic. If a function is deterministic, its tests need inputs and expected outputs, not four mocks.

Restricted-environment variant

Client contract forbids sending source to external APIs? Use the client-sanctioned enterprise assistant (Copilot Business with the no-training flag, or Bedrock/Azure OpenAI inside the client tenancy). If none exists: extract the function signature, docstring, and branch structure into a sanitized skeleton with neutral names, generate tests against the skeleton, then rename locally. Local fallback — Qwen-Coder-class models via Ollama handle single-function test generation adequately; keep the curate-and-mutate steps identical, since they run entirely on your machine.

Warning

Test fixtures often contain production-shaped data. Never paste real customer records into a hosted tool to "make the fixtures realistic" — see pb-29 for synthetic data that respects your schema.

Success criteria

  • The target module's mutation score cleared your threshold and is recorded in the PR.
  • You deleted at least some generated tests — proof that curation actually happened.
  • A subsequent refactor of the module is caught by the new suite when you introduce a deliberate bug.
  • Time from "no tests" to "gated suite" was hours, not the sprint it used to take.
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