Testland
Browse all skills & agents

pitest-mutation

Configures PIT (PITest) for mutation testing of JVM projects (Java, Kotlin via the Kotlin plugin) - wires the `pitest-maven` or `pitest-gradle-plugin` with `mutationThreshold`, `coverageThreshold`, target classes/tests filtering, runs `mvn pitest:mutationCoverage`, parses the HTML + XML reports. Use when the JVM suite needs mutation-quality verification - the canonical Java mutation testing tool, fast (PIT analyzes "in minutes rather than days").

Install with skills.sh (any agent)

npx skills add testland/qa --skill pitest-mutation
View source

pitest-mutation

Overview

Per pit-home (opens in new window):

"PIT is a mutation testing system for Java and JVM applications. It automatically introduces faults into code, then runs tests to see if they catch these modifications."

"Faults (or mutations) are automatically seeded into your code, then your tests are run. If your tests fail then the mutation is killed, if your tests pass then the mutation lived." (pit-home (opens in new window))

Per pit-home (opens in new window), PIT differentiates on speed: "analyzes in minutes rather than days" vs older mutation tools.

When to use

  • A JVM project (Java, Kotlin via plugin) needs mutation-quality verification.
  • A team wants to identify "partial testing that traditional metrics miss" (pit-home (opens in new window)).
  • Maven / Gradle integration is required (PIT is first-class for both).

How to use

  1. Add the pitest-maven (or info.solidsoft.pitest Gradle) plugin, including pitest-junit5-plugin for JUnit 5 suites.
  2. Point targetClasses at production packages and targetTests at the matching test packages - never mutate test code.
  3. Set mutationThreshold and coverageThreshold to the current baseline and request HTML + XML outputFormats.
  4. Run mvn pitest:mutationCoverage (or ./gradlew pitest) and open target/pit-reports/<timestamp>/index.html.
  5. For each surviving mutant, add or strengthen a test until it is killed.
  6. Use -DwithHistory for PR runs so only changed code is re-mutated.
  7. In CI, run full weekly and incremental per-PR, uploading target/pit-reports/ as an artifact.

Step 1 - Install (Maven)

<plugin>
  <groupId>org.pitest</groupId>
  <artifactId>pitest-maven</artifactId>
  <version>1.17.0</version>
  <configuration>
    <targetClasses>
      <param>com.example.checkout.*</param>
    </targetClasses>
    <targetTests>
      <param>com.example.checkout.*Test</param>
    </targetTests>
    <mutationThreshold>75</mutationThreshold>
    <coverageThreshold>80</coverageThreshold>
    <outputFormats>
      <format>HTML</format>
      <format>XML</format>
    </outputFormats>
  </configuration>
  <dependencies>
    <dependency>
      <groupId>org.pitest</groupId>
      <artifactId>pitest-junit5-plugin</artifactId>
      <version>1.2.1</version>
    </dependency>
  </dependencies>
</plugin>

For JUnit 5, the pitest-junit5-plugin is required; for JUnit 4 the default plugin works.

Step 2 - Run

mvn pitest:mutationCoverage

Reports land at target/pit-reports/<timestamp>/. The index.html shows per-class mutation coverage.

Step 3 - Configure (Gradle)

plugins {
    id 'java'
    id 'info.solidsoft.pitest' version '1.15.0'
}

pitest {
    targetClasses = ['com.example.checkout.*']
    targetTests = ['com.example.checkout.*Test']
    mutationThreshold = 75
    coverageThreshold = 80
    outputFormats = ['HTML', 'XML']
    junit5PluginVersion = '1.2.1'
}

// Run via:
// ./gradlew pitest

Step 4 - Mutators

PIT's default mutator set covers conditional, arithmetic, return value, void method calls, and constructor calls. Activate additional mutator sets via <mutators>:

<mutators>
  <mutator>STRONGER</mutator>     <!-- All default + extra -->
  <mutator>DEFAULTS</mutator>     <!-- Default set -->
  <mutator>ALL</mutator>          <!-- Everything -->
</mutators>

Per pit-home (opens in new window), reports "combine line coverage with mutation coverage data."

Step 5 - pitmp-maven-plugin for incremental runs

For PRs, only mutate changed code:

mvn pitest:mutationCoverage -DwithHistory

withHistory reads / writes a history file in target/; subsequent runs only mutate code different from the cached history. For PR runs, combine with git diff to scope further:

mvn pitest:mutationCoverage \
  -Dfeatures='+gitci(level[1])' \
  -Dpitmp.git.diff.target=origin/main

(The pitmp-maven-plugin extension adds git-diff-based scoping; not in core PIT.)

Step 6 - CI integration

- uses: actions/setup-java@v4
  with: { distribution: temurin, java-version: '21' }

- name: Mutation testing (full)
  if: github.event_name == 'schedule'  # weekly cron
  run: mvn pitest:mutationCoverage

- name: Mutation testing (incremental, PR)
  if: github.event_name == 'pull_request'
  run: mvn pitest:mutationCoverage -DwithHistory

- uses: actions/upload-artifact@v4
  if: always()
  with:
    name: pit-reports
    path: target/pit-reports/

The XML output (mutations.xml) is machine-parseable for dashboards.

Step 7 - Kotlin support

PIT works with Kotlin via standard Maven/Gradle Kotlin plugins; mutators apply to compiled bytecode. Per pit-home (opens in new window), "ArcMutate, from the same team, extends PIT with Kotlin support, Spring integration, and Git analysis" - for richer Kotlin / Spring support, evaluate ArcMutate (commercial).

Worked example

CheckoutService in com.example.checkout has a free-shipping rule and green JUnit 5 tests.

  1. Wire the plugin (Step 1) with targetClasses = com.example.checkout.*, targetTests = com.example.checkout.*Test, mutationThreshold = 75.
  2. mvn pitest:mutationCoverage writes target/pit-reports/<timestamp>/index.html and fails the build below the 75 threshold.
  3. index.html flags a survivor: the free-shipping boundary total >= 50 mutated to total > 50, uncaught because no test uses total == 50.
  4. Add a JUnit 5 test at the total == 50 boundary; mvn pitest:mutationCoverage now kills the mutant and clears the threshold.
  5. Switch the PR job to -DwithHistory so later runs only re-mutate changed classes.

Pitfalls and limitations

Anti-patterns (mutating everything per PR, mutationThreshold: 100, mixed Maven/Gradle config, missing JUnit 5 plugin, targeting test classes, the ALL mutator set) and PIT's limitations (bytecode-level equivalent mutants, build-tool coupling, commercial Kotlin / Spring extras, per-class scope) are catalogued in references/pitest-pitfalls.md.

References

  • pit (opens in new window) - PIT overview: mutation testing definition, JVM-only, speed claim ("minutes rather than days"), Maven/Gradle/Ant integration, ArcMutate Pro extension.
  • stryker-mutation, stryker-net-mutation, mutmut-mutation, mull-mutation - per-language siblings.

PIT - anti-patterns and limitations

View source (opens in new window)

PIT - anti-patterns and limitations

Detailed pitfalls and constraints for pitest-mutation. Linked inline from that skill's SKILL.md.

Anti-patterns

Anti-patternWhy it failsFix
Mutating the entire codebase every PRSlow; team disables.withHistory + per-changed-file scope.
mutationThreshold: 100Unreachable; first failed run blocks all PRs.Start at the current baseline; ratchet up.
Mixed Maven/Gradle config (both plugins active)Conflicting configurations; cryptic errors.Pick one build tool.
Missing JUnit 5 plugin dependencyTests don't run; mutation coverage 0.Add pitest-junit5-plugin for JUnit 5.
Targeting test classes in targetClassesMutates test code; meaningless.targetClasses = production package; targetTests = test package.
All mutators (ALL mutator set)Many irrelevant mutants; long runtime.DEFAULTS (default) or STRONGER.

Limitations

  • Bytecode-level. PIT mutates compiled .class files; some language-level constructs (Kotlin sealed classes, certain generics) produce equivalent mutants.
  • Build-time integration. Requires Maven / Gradle; standalone PIT is awkward for Bazel / Pants.
  • Commercial Kotlin / Spring features. ArcMutate is paid; open-source PIT covers the basics.
  • Per-class scope only. No file-level or method-level scoping out of the box; use targetClasses patterns.

Related skills

mull-mutation

Runs Mull, the LLVM-IR mutation testing tool, against C/C++ test binaries built with Clang: covers install (the version-matched mull-NN package), the -fpass-plugin build flags for the Mull IR frontend, mull-runner invocation, the mutator catalog, path filtering, and GitHub Actions CI. Use when a C or C++ project needs mutation-score verification with the tool already chosen. Does not select among mutation tools and does not cover other languages (stryker-mutation for JS/TS, stryker-net-mutation for .NET, pitest-mutation for the JVM, mutmut-mutation for Python).

mutant-survival-triage

Normalizes a surviving-mutant record across StrykerJS, PIT, mutmut, and Mull into one shape, classifies why it survived (missing case, weak assertion, equivalent mutant, unreachable code, flaky killer), applies per-mutator heuristics for conditional-boundary, arithmetic-operator, statement-removal, and constant mutations, and drafts the specific test that would kill it. Treats equivalence as a judgment call, because deciding whether a mutant is equivalent to the original is undecidable in general, so a residual survivor rate is expected rather than a defect. Use when a mutation run has finished and the report lists surviving mutants that nobody has yet explained or turned into concrete test cases.

mutmut-mutation

Configures mutmut for Python mutation testing - `pip install mutmut`, runs via `mutmut run`, browses results via `mutmut browse` or `mutmut results`, applies surviving mutants to disk via `mutmut apply {id}`, suppresses with `# pragma: no mutate` annotations. Configures via `setup.cfg` / `pyproject.toml` with `source_paths` + per-test selection. Use for Python codebases needing mutation-quality verification of pytest / unittest suites.

stryker-mutation

Configures StrykerJS for mutation testing of JavaScript / TypeScript / React / Vue / Svelte / Node - picks the test-runner plugin (`@stryker-mutator/jest-runner`, `mocha-runner`, `vitest-runner`, `karma-runner`), authors `stryker.conf.json` with mutate globs + thresholds, runs incremental mode for PRs (only mutate changed files), and reports the mutation score. Use when a JS/TS test suite has ≥80% line coverage and the team wants to verify the tests actually catch bugs (not just touch lines).

stryker-net-mutation

Configures Stryker.NET for mutation testing of .NET Core / .NET Framework projects - installs `dotnet-stryker` global tool, scopes mutation to specific csproj, supports xUnit / NUnit / MSTest, authors `stryker-config.json` with thresholds, runs in CI. Use when a .NET test suite needs mutation-quality verification - closes the .NET ecosystem gap left by Stryker.NET being newer than the JS variant.