PIT measures whether your Java tests actually assert anything
State of the art mutation testing system for the JVM
At a glance
- What is it?
- PIT is a mutation testing system for Java and the JVM that rewrites bytecode into faulty versions of your code and checks whether your test suite notices. It suits a team with line coverage that wants to know whether the coverage means anything, and a fast test suite is important because each mutant may require additional test execution.
- Who is it for?
- Adopt PIT when your Java test suite is fast and you already measure coverage, since its output is only interpretable next to a coverage number and its cost scales with the number of mutants. Use Dry Run if you want to gather coverage information without running mutant analysis. and do not run it on a suite that takes minutes without expecting the run to take hours.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 9 days ago.
- What is it written in?
- Mainly Java, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Star history
- hcoles/pitest1,877 ★ · +1
| Repository | 2026-10-16 | 2026-10-22 |
|---|---|---|
| hcoles/pitest | 1.9k | 1.9k |
1 Stars in the last 6 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
What mutation testing answers that coverage cannot
PIT, short for Pitest, is described as a state of the art mutation testing system for Java and the JVM. The documentation site is pitest.org, and the Maven Central artifact is `org.pitest:pitest`.
The method is the classic one. PIT takes compiled bytecode, makes small deliberate faults in it, and runs your existing test suite against each faulty version. If the tests still pass, the mutant survived, and the surviving mutant marks a place where your suite asserted nothing. If a test fails, the mutant was killed, and you have evidence that the test genuinely checks that behaviour.
That distinction is the whole argument for the tool. Line coverage answers whether a line ran. A surviving mutant answers whether the assertion on that line could fail. A file can be 100 percent covered and contain mutants that survive every test, and only mutation testing finds them.
The cost model follows directly from the method, and it is worth being explicit before anyone tries it. PIT runs your tests once for every mutant it creates. Mutation testing can be expensive because each mutant may require test execution.
This is why PIT and a coverage tool belong together. Coverage tells you where the mutants will be, because a line with no coverage generates no mutant. Reporting only on covered code is both faster and the only meaningful way to read the output.
The repository is a multi-module build, not a single jar
The top-level layout explains more about the project's architecture than the README does, because the README is largely a changelog.
There is a `pitest/` directory holding the engine, surrounded by the modules that package it for different consumers: `pitest-entry/` for programmatic entry, `pitest-command-line/`, `pitest-maven/` for the Maven plugin, `pitest-ant/` for Ant, and `pitest-html-report/` for reporting. Two verification modules sit alongside: `pitest-maven-verification/` and `pitest-modern-verification/`, which reads as a split between a legacy compatibility baseline and current behaviour. `pitest-aggregator/` combines results across modules, and `pitest-build-config/` holds shared build settings.
Three documentation files are worth knowing about before reading any source. `so_you_want_to_build_mutation_testing_system.md` explains the mechanism, `hackers_guide.md` is the developer-facing guide, and `ISSUES.md` is a tracked list of known problems rather than an issue tracker in the usual sense.
The build is Maven-based with the wrapper checked in, as `mvnw` and `mvnw.cmd`. Continuous integration runs on Azure Pipelines through `azure-pipelines.yml`, and there is a `.mvn/` directory for wrapper configuration.
There is also a `samples/` directory with its own `pom.xml` and sources, which is the practical place to see a working configuration rather than reading one described in prose. `release-branch.sh` alongside it suggests releases are cut by a script rather than by hand.
Licence is Apache 2.0, in `LICENSE.txt`.
The changelog is the real documentation, and it explains the behaviour changes
Almost all of this README is a release history, and reading it as one is more informative than skimming it. Several entries change what PIT does by default rather than adding features, and those are the ones that break assumptions when you upgrade.
Two are worth reading closely. In 1.15.7, coverage was not recorded for code in packages named `com.sun*`: a filter meant to pick up legacy JDK classes was missing a quote character, so if the code under test happened to be in a package starting with `sun`, no coverage was registered at all. That is the kind of bug that looks like your tests passing for the wrong reason.
In 1.25.7, JaCoCo transformations are disabled at bytecode level, following 1.25.6 which auto-disables the Quarkus JaCoCo extension. So if you run PIT and JaCoCo together under Quarkus, one of them now steps aside.
Another default-off item: 1.15.8 added filtering for `Collections.unmodifiable*`, off by default and activated with the filter string `+funmodifiablecollection`, extended in 1.16.0 to cover unmodifiable collections stored directly into fields for defensive code. That is a filter, not a feature, and a filter is a judgement call about whether defensive copies should count.
The changelog also records a version-numbering mistake. The README states that the jump from the 1.25.x series was due to mislabelling of a release as 1.29.10. If you see a 1.29.x artifact, that is why.
And one API warning, from 1.16.0: the BuildVerifier interface expansion was backwards compatible, but plugins written against 1.16.0 may not work with earlier versions. That is the direction to watch if you write PIT plugins.
Interoperability with build tools is where most of the work goes
A large fraction of the recent changelog is about cooperating with other tools, which is a fair signal of where PIT's real complexity lives. Mutation testing is easy in principle; making it work inside Maven, Surefire, Quarkus and JaCoCo is not.
The Surefire interaction appears repeatedly because that is how JVM tests are launched. Entries include handling args files in the argLine, using environment variables from Surefire config, automatically adding `-ea` to enable assertions, expanding `${settings.localRepository}` in `<argLine>`, quoting classpath entries to handle spaces, and handling spaces in agents and variables inherited from launch.
There is a new Maven property, `parseSurefireArgLine`, introduced in 1.15.8 and set to true by default, which can be set to false to disable Surefire argline parsing. So the behaviour that matters most for compatibility has an explicit off switch.
The Quarkus story has its own requirement. 1.19.4 adds support for Quarkus 3.22 and above, with the note that Quarkus support also requires version 1.2.3 or above of the PIT JUnit 5 plugin. So a Quarkus user needs two aligned upgrades, not one.
Kotlin gets first-class treatment too. 1.16.3 auto-adds standard Kotlin source directories for Maven projects, and 1.20.3 auto-adds JUnit engines when missing from the classpath.
The pattern across all of it: PIT launches your tests itself, which means it has to reproduce whatever your build tool does to launch them. Most of the bug fixes are that reproduction getting more faithful.
Extension points, filters, and a developer guide for contributors
PIT has grown a substantial plugin surface, which the changelog documents in the same terse issue-number style.
Extension points added over the recorded history include a test filter extension point in 1.22.0, a mutator URL extension point in 1.19.0, a transformation extension point in 1.17.0, a project filtering extension point in 1.24.0, an extension point for test stat listeners in 1.20.0, and an extension point to allow custom coverage export formats in 1.15.4.
Several entries are about making history available to other parts of the system. 1.25.0 makes history available to interceptors and introduces an equivalent status. 1.25.3 introduces a post pre-scan type, and 1.23.0 moves default history analysis to a plugin. 1.25.1 makes report options available to history plugins and adds a progress report.
Interceptors and history together explain how PIT handles constructs that defeat naive mutation. Changelog entries reference filtering based on a marker line number, filtering mutants in an enum switch default block, normalising record attribute order on first load, mutating delayed execution code in enums and singletons, and improved delayed execution detection in static initialisation code. Records, enums, static initialisers and switch defaults are exactly the places where a mutant is either unkillable or misleading, and each has needed dedicated handling.
One entry is worth calling out as a warning sign. 1.15.6 temporarily removes dynamic call tracking for static initialiser code, with the note that it will be reintroduced when additional logic to suppress mutation filtering for delayed execution code has been implemented. A temporary removal that is still described as pending is a known limitation.
The 1.24.0 entry for silent mode is relevant if you want to run PIT in a pipeline where a mutant report on stdout is noise.
Practical guidance, and where mutation testing stops being worth it
Putting the pieces together, the sensible way to run PIT is narrower than running it on everything.
Scope it to covered code. Mutants in uncovered lines are killed by nothing and tell you nothing you did not already know from the coverage report. The cost is roughly proportional to covered lines times mutants per line, so narrowing the target is the only real lever on runtime.
Run it on the module you changed rather than the whole reactor. The `pitest-aggregator/` module exists to combine results across modules, which tells you multi-module aggregation is supported, but aggregating is for trend reporting, not for a fast inner loop.
Expect build-tool friction first, not mutation results. Given the changelog, the realistic first-run experience is a Surefire argLine, an arg file, or a classpath with a space in it. That is a configuration problem, and the `samples/` project is the place to resolve it.
Three cases where it is the wrong investment. A project with no test suite gets no value, because there are no assertions to fail. A suite that is slow or flaky will produce results you cannot trust; a mutant that appears killed because of a flaky test is worse than no result. And generated code, configuration classes and pure DTOs will generate survivors forever unless you filter them, which is what the filter strings and the project filtering extension point exist for.
The dry run mode added in 1.17.3 is the tool for the first attempt. Maven users can gather coverage information and generate mutants but skip the mutant analysis stage by setting the `pit.dryRun` flag to true. That lets you measure coverage, count mutants and estimate runtime without paying for the analysis.
Maintenance and dependency hygiene
The project is actively releasing, and the dependency bumps are the clearest evidence.
Recent releases are 1.30.0 on 2026-08-27 and 1.30.1 on 2026-10-14, with the last push on 2026-10-14. The 1.30.1 entries are almost entirely dependency work: adding the Plexus XML module required by Plexus Utils, bumping `org.codehaus.plexus:plexus-utils` to 4.0.3, bumping ASM to 9.11, and bumping Jackson to 2.22.3.
That ASM number is worth pausing on. PIT rewrites bytecode, so ASM is a hard dependency on the JVM's class file format, not an incidental library. 1.30.1 taking ASM 9.11 is a signal about which JDK releases PIT currently parses. Going back through the history, ASM was bumped to 9.9 in 1.20.5, 9.8 in 1.19.6, 9.10.1 in 1.25.6, and 9.11 now, which is a reliable way to see which Java version each release targeted.
Java 25 support shows up in 1.25.8, with fixes for the BigDecimal and BigInteger mutators for Java 25. The minimum runtime was raised to Java 11 in 1.18.0. So the supported floor is Java 11 and the ceiling moves with the JDK releases.
Two supply-chain entries are also worth noting. 1.25.2 publishes SBOMs via CycloneDX, and 1.19.2 switched to central distribution before Sonatype EOL. For a tool that gets injected into a build, both are relevant to anyone with an artefact policy.
The project also documents that 1.18.2 and 1.18.1 fixed classpath quoting and agent path spaces, and 1.18.0 ensures agent jar files are removed at end of session. That last one is a cleanup fix, and cleanup fixes in tooling tend to matter more than the changelog suggests.
Editorial conclusion
Adopt PIT when your Java test suite is fast and you already measure coverage, since its output is only interpretable next to a coverage number and its cost scales with the number of mutants. Use Dry Run if you want to gather coverage information without running mutant analysis. and do not run it on a suite that takes minutes without expecting the run to take hours. Before the first run, read the hackers guide in the repository and the page on building a mutation testing system, because the changelog shows the default behaviour has changed across versions, including coverage handling for `com.sun` packages, JaCoCo transformation, and unmodifiable collection filtering that is off by default.
Frequently asked questions
What does PIT do and what problem does it solve?
PIT is a mutation testing system for Java and the JVM. It makes deliberate faults in your compiled bytecode and runs your test suite against each faulty version, so a surviving mutant shows you a place where your tests asserted nothing and line coverage was misleading.
How do I run PIT against my Maven project?
The README documents the Maven goal, the Surefire argLine handling and the `parseSurefireArgLine` property, and points to the documentation site at pitest.org. The repository also ships a `samples/` project with its own `pom.xml` that is the practical place to see a working configuration.
What is PIT's dry run mode for?
Added in 1.17.3, it lets Maven users gather coverage information and generate mutants but skip the mutant analysis stage by setting the `pit.dryRun` flag to true. It lets you measure coverage and count mutants without paying for the full run.
Does PIT work with Quarkus and JUnit 5?
Version 1.19.4 added support for Quarkus 3.22 and above, and the release note says Quarkus support also requires version 1.2.3 or above of the pitest JUnit 5 plugin. Version 1.25.6 auto-disables the Quarkus JaCoCo extension, and 1.25.7 disables JaCoCo transformations at bytecode level.
What is the minimum Java version PIT supports?
Version 1.18.0 updated the minimum supported Java runtime to 11. Support for newer runtimes moves with the ASM bumps, and 1.25.8 included fixes for the BigDecimal and BigInteger mutators for Java 25.
How do I stop PIT mutating defensive copies?
The `Collections.unmodifiable*` filter was added in 1.15.8 and is off by default; you activate it with the filter string `+funmodifiablecollection`. Version 1.16.0 extended it to unmodifiable collections stored directly into fields for defensive code.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/hcoles-pitest)Star history chart for your README
[](https://hysenlabs.com/projects/hcoles-pitest)Was this useful?