Mutation Testing
What it is
Section titled “What it is”Mutation testing measures the quality of your tests, not their coverage. The tool deliberately injects small faults (mutants) into your source — flip a comparison, remove a call, change a constant — then runs your test suite.
If the tests fail, the mutant is killed (good). If they still pass, the mutant survived, and that line is a hole in your suite.
original code ──► mutate ──► mutant │ │ │ ▼ │ run test suite │ │ │ ┌───────────┴───────────┐ │ ▼ ▼ │ tests fail tests pass │ (mutant killed ✓) (mutant survived ✗) │ └── mutation score = killed / total mutantsA mutation score near 100% means your assertions actually detect change; line coverage can be 100% while the mutation score is low — that is exactly the gap coverage hides.
When to use it / when to avoid it
Section titled “When to use it / when to avoid it”Use it for
- Critical modules: pricing, auth, money, permissions, parsers
- Auditing tests you inherited and do not trust
- Finding assertions that run but never check anything
- A periodic quality gate, not an every-commit one
Avoid it when
- The suite is slow — mutation testing multiplies runtime by the number of mutants
- Code is trivial glue, generated, or has no branching to mutate
- You run it on the whole repo every commit instead of a scoped subset
- You chase a 100% score and start writing tests that only kill noise
Frameworks & tools
Section titled “Frameworks & tools”| Tool | Ecosystem | Notes |
|---|---|---|
| Stryker | JS/TS, C#, Scala | Rich reporter, incremental mode, dashboard |
| PIT (pitest) | Java/JVM | Fast, mature, plugin ecosystem |
| mutmut | Python | Simple CLI; parallel mutation |
| cosmic-ray | Python | Pluggable mutators, SQLite session store |
| cargo-mutants | Rust | Zero-config, respects cargo test |
| go-mutesting | Go | Mutation for Go packages |
| Mutmut / mutmut2 | Python | Community continuation of mutmut |
Quick setup
Section titled “Quick setup”npm install -D @stryker-mutator/core @stryker-mutator/vitest-runnernpx stryker runpip install mutmutmutmut runmutmut resultsgo install github.com/zimmski/go-mutesting/cmd/go-mutesting@latestgo-mutesting ./...<!-- Maven: PIT plugin --><plugin> <groupId>org.pitest</groupId> <artifactId>pitest-maven</artifactId> <version>1.17.2</version></plugin>dotnet tool install -g dotnet-strykerdotnet strykerExample
Section titled “Example”{ "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json", "testRunner": "vitest", "mutate": ["src/**/*.ts", "!src/**/*.test.ts"], "thresholds": { "high": 90, "low": 80, "break": 70 }, "reporters": ["html", "clear-text", "progress"]}# survive only on code the tests actually exercisemutmut run --paths-to-mutate src/discount.py --tests-dir testsmutmut show # inspect surviving mutantsgo-mutesting --exec "go test ./discount/..." ./discount/...# reports mutants that survived the suite<plugin> <groupId>org.pitest</groupId> <artifactId>pitest-maven</artifactId> <version>1.17.2</version> <configuration> <targetClasses><param>com.example.discount.*</param></targetClasses> <mutationThreshold>80</mutationThreshold> </configuration></plugin>{ "stryker-config": { "project": "Discount.csproj", "thresholds": { "high": 90, "low": 80, "break": 70 } }}Anti-patterns & best practices
Section titled “Anti-patterns & best practices”- Do not run it everywhere, every time. Scope it to critical modules and run it nightly or pre-release; it multiplies your test runtime.
- Use incremental mode. Stryker and PIT can cache unchanged mutants so repeat runs are cheap.
- Prioritise survived mutants with real impact. A surviving mutant in a boundary check matters more than one in a log message.
- Do not chase 100%. Equivalent mutants (same behaviour, different code) can never be killed; treat the score as a signal, not a target.
- Fix tests, not the tool. If a mutant survives, add or tighten an assertion — never suppress it to raise the number.
- Pair with coverage. Coverage tells you what ran; mutation testing tells you what was actually verified.
Self-assessment quiz
Section titled “Self-assessment quiz”Self-assessment quiz
Pick one answer per question, then press Check answers.