| name | pit-mutation-tuning |
| description | Configure and interpret PIT mutation testing scoped to changed code. Use when adding the mutation gate to a project, when interpreting `mutations.xml`, or when tuning thresholds. |
| when_to_use | ["Phase 5/6 — running `mvn -Ppit pitest:mutationCoverage` and analyzing results.","Adding mutation testing to a brownfield project (incremental scope to avoid 30-minute waits)."] |
| authoritative_references | ["https://pitest.org/","https://github.com/hcoles/pitest"] |
PIT mutation tuning
Why mutation testing
Coverage proves the code ran. Mutation proves the test would have caught a real defect. A mutant is a small change (negate a condition, return null, change > to <=); a "killed" mutant means a test failed; a "survived" mutant means your test suite is blind.
Default configuration
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.17.0</version>
<configuration>
<targetClasses>
<param>com.example.shop.*</param>
</targetClasses>
<targetTests>
<param>com.example.shop.*</param>
</targetTests>
<mutators>
<mutator>STRONGER</mutator>
</mutators>
<outputFormats>
<param>HTML</param>
<param>XML</param>
</outputFormats>
<timestampedReports>false</timestampedReports>
<features>
<feature>+GIT(from[HEAD~1])</feature>
</features>
<mutationThreshold>80</mutationThreshold>
<coverageThreshold>90</coverageThreshold>
</configuration>
<dependencies>
<dependency>
<groupId>org.pitest</groupId>
<artifactId>pitest-junit5-plugin</artifactId>
<version>1.2.1</version>
</dependency>
</dependencies>
</plugin>
Scope: incremental, not full
Full PIT runs are slow. Always scope to changed code:
- Locally / in
/build refactor phase: +GIT(from[HEAD~1]).
- In CI for PRs:
+GIT(from[origin/main]).
- Nightly: full run, results posted to dashboard but not blocking.
What to do with surviving mutants
For every survived mutant in changed packages:
- Look at the mutated line and the mutation type (e.g. "negated conditional").
- Write a test that would have failed under the mutation.
- Re-run; the mutant should now be killed.
- Log the new test in
06-test-plan.md.
If a mutant is genuinely equivalent (impossible to kill — e.g. a defensive if that can never be false), suppress it via @CoverageIgnore on the specific method, with a comment explaining why. Track in 08-code-review.md waivers.
Common mutation types worth caring about
| Mutator | Example | What it tells you |
|---|
NegateConditionals | if (x > 0) → if (x <= 0) | Boundary tests missing. |
ReturnVals | return x; → return null; | Caller doesn't validate non-null. |
MathMutator | a + b → a - b | Arithmetic untested with non-zero values. |
VoidMethodCalls | removes a call | Side effect not asserted. |
Reading mutations.xml
<mutation status="SURVIVED" detected="false">
<sourceFile>PriceCalculator.java</sourceFile>
<mutatedClass>com.example.shop.checkout.PriceCalculator</mutatedClass>
<mutatedMethod>apply</mutatedMethod>
<mutator>org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator</mutator>
<description>negated conditional</description>
<lineNumber>42</lineNumber>
</mutation>
spring-validator parses this and lists every SURVIVED mutant in changed files in 07-validation-report.md.
Anti-patterns
- Lowering
mutationThreshold to make the build green.
- Excluding entire packages because they're "hard to test".
- Marking equivalent mutants without a one-line justification.
- Running PIT against the whole codebase on every build.