JVM Code Coverage Tools Compared: A Pragmatic Guide
Code coverage is one of those metrics that sounds simple but gets misused constantly. Teams chase 100% coverage as though it were a badge of engineering excellence, when in practice it often leads to the opposite: brittle test suites full of meaningless assertions, written to satisfy a number rather than to catch bugs.
Before comparing the tools available on the JVM, it’s worth establishing what code coverage actually tells you — and more importantly, what it doesn’t.
Code Coverage: What It Is and What It Isn’t
Code coverage measures which lines, branches, or instructions in your codebase were executed during a test run. That’s it. It tells you what code was touched, not whether it was tested meaningfully. A test that calls a method and ignores the result achieves the same coverage as one with rigorous assertions.
“A useful tool for finding untested parts of a codebase, but of little use as a numeric statement of how good your tests are.”
— Martin Fowler, software design author and ThoughtWorks chief scientist (Test Coverage, 2012)
The distinction matters. Coverage is a diagnostic tool, not a quality metric.
“Program testing can be used to show the presence of bugs, but never to show their absence.”
— Edsger Dijkstra, Turing Award recipient and pioneering computer scientist (Notes on Structured Programming, 1970)
Coverage narrows this further — it shows the presence of execution, not the presence of verification.
Why 100% Coverage Is the Wrong Target
Pursuing 100% coverage creates the wrong incentives. Developers write tests to hit lines rather than to validate behaviour. Mark Seemann [author of Dependency Injection in .NET] demonstrated this convincingly — a test that wraps every call in a try/catch with no assertions achieves full coverage with zero value (Code Coverage Is a Useless Target Measure, 2015). Worse, adding genuinely better tests — boundary cases, edge conditions — often adds no additional coverage over a minimal test that already touched the same lines.
“I get paid for code that works, not for tests, so my philosophy is to test as little as possible to reach a given level of confidence.”
— Kent Beck, creator of Extreme Programming and Test-Driven Development
His advice is to write tests until fear is transformed into boredom — then stop.
“Am I suggesting 100% test coverage? No, I’m not suggesting it. I’m demanding it.”
— Robert C. Martin, author of Clean Code and software craftsmanship advocate
But he immediately qualifies this as an asymptotic goal (something you approach but never fully reach), warning that coverage is “a very bad management metric” — managers who enforce targets incentivise developers to pad the number with meaningless tests rather than write genuine assertions.
Brian Marick [Agile Manifesto co-author and testing expert] documented this dynamic in his influential paper How to Misuse Code Coverage (1999): organisations that mandated a specific coverage percentage reliably got exactly the number they asked for — a clear sign of gaming rather than genuine improvement.
Coverage targets can actively inhibit good engineering. If adding a defensive null check or input validation to production code lowers your coverage percentage (because there’s no test that exercises the new failure path), developers are incentivised to leave the guard out. That’s the opposite of what you want.
What Good Coverage Practice Looks Like
Google’s internal research across a billion lines of code established tiered benchmarks: 60% is “acceptable”, 75% is “commendable”, and 90% is “exemplary” (Code Coverage at Google, ESEC/FSE 2019). Codecov’s analysis of open-source repositories found that most cluster around 80%, with quality of tests declining noticeably above that threshold (The Case Against 100% Code Coverage).
The practical approach:
- Use coverage to find gaps, not to prove quality. If a critical payment processing path shows 0% coverage, that’s a problem. If a simple DTO (Data Transfer Object) constructor is uncovered, it probably doesn’t matter:
public record UserDTO(String name, String email) {}
Writing a test like this just to bump the coverage number is pure waste:
@Test
void testUserDTO() {
var user = new UserDTO("Alice", "alice@example.com");
assertEquals("Alice", user.name());
assertEquals("alice@example.com", user.email());
}
- Test the areas of highest risk. Business logic, data transformations, security boundaries, and integration points deserve thorough testing. Getters, setters, and trivial delegation do not.
- Monitor trends, not absolutes. A dropping coverage percentage on actively developed code is a warning sign. A stable 78% with well-targeted tests beats a forced 95% with padding.
- Consider mutation testing for validating test quality rather than just execution (more on this below).
With that context, let’s look at what’s available on the JVM.
JaCoCo: The Industry Standard
JaCoCo (Java Code Coverage) is the dominant coverage tool in the JVM ecosystem and the one most teams will encounter. It works through bytecode instrumentation — a Java agent intercepts class loading and injects coverage probes into compiled bytecode at runtime.
Configuration
JaCoCo integrates natively with both major build tools. In Gradle, the plugin ships with Gradle itself:
plugins {
jacoco
}
tasks.jacocoTestReport {
reports {
xml.required.set(true)
html.required.set(true)
}
}
tasks.jacocoTestCoverageVerification {
violationRules {
rule {
limit {
minimum = "0.75".toBigDecimal()
}
}
}
}
Maven integration uses the official jacoco-maven-plugin:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
What It Measures
JaCoCo tracks six metrics: instructions, lines, branches, cyclomatic complexity, methods, and classes. Reports are generated in HTML, XML, and CSV formats. The HTML reports are clear and navigable, showing coverage at package, class, and line level with colour-coded source views.
Strengths
- Ecosystem integration is unmatched. SonarQube, Codecov, GitHub Actions, GitLab CI, and virtually every CI/CD tool supports JaCoCo natively.
- Minimal overhead. On-the-fly instrumentation avoids modifying class files on disk and adds negligible runtime cost.
- Zero source dependency. Works entirely from compiled bytecode, so it handles any JVM language that produces standard class files.
- Mature and stable. Actively maintained, with version 0.8.14 released in October 2025.
Limitations
- Kotlin inline functions are not correctly handled. Inlined code executes in the calling class rather than the declaring class, and JaCoCo cannot reconcile this. The maintainers have stated a fix is unlikely to be implemented.
- Bytecode-level metrics can be confusing. The “instruction” count doesn’t correspond to source lines, and branch counts on a single line can be hard to interpret.
- No test-level granularity out of the box. JaCoCo tells you what was covered, not which test covered it.
Verdict
JaCoCo is the safe, well-supported default for Java projects. If you’re working primarily in Java and need broad tooling compatibility, it remains the right choice.
Kover: Built for Kotlin
Kover is JetBrains’ answer to JaCoCo’s Kotlin limitations. Rather than being a standalone instrumentation engine, Kover is a build tool plugin that delegates to either the IntelliJ coverage agent or JaCoCo as a backend, while providing a Kotlin-aware configuration layer.
Configuration
plugins {
id("org.jetbrains.kotlinx.kover") version "0.9.7"
}
kover {
reports {
verify {
rule {
minBound(75)
}
}
}
}
Kover also provides a Maven plugin (kover-maven-plugin) and a standalone CLI tool for offline instrumentation.
Strengths
- Kotlin-native. Correctly handles inline functions, coroutines, and other Kotlin-specific constructs that JaCoCo struggles with.
- Dual backend. Can use the IntelliJ agent (better Kotlin support) or JaCoCo (better ecosystem compatibility), selectable per project.
- Android support. Works with Kotlin Android, multiplatform, and standard JVM projects.
- Verification rules built into the plugin — set coverage thresholds directly in your build script without additional configuration.
Limitations
- JVM and Android only. Kotlin/JS and Kotlin/Native targets are not supported.
- Smaller ecosystem. Fewer CI/CD tools consume Kover’s output natively compared to JaCoCo, though its JaCoCo-compatible XML output mitigates this.
- Beta stability. Still at Kotlin Beta status, meaning API changes between versions are possible.
- Instrumentation side effects. The IntelliJ agent can occasionally affect concurrent test execution or cause performance degradation under heavy load.
Verdict
For Kotlin or mixed Kotlin/Java projects, Kover is the best choice in 2026. Its understanding of Kotlin semantics eliminates the false negatives that plague JaCoCo on Kotlin codebases. The ability to fall back to JaCoCo as a backend means you don’t sacrifice CI/CD compatibility.
OpenClover: Source-Level Instrumentation
OpenClover takes a fundamentally different approach. Where JaCoCo and Kover instrument bytecode, OpenClover modifies source code before compilation, inserting coverage recording statements directly into Java, Groovy, and AspectJ files.
This gives OpenClover visibility into source-level constructs that bytecode tools can miss. Its HTML reports were historically the richest in the ecosystem, with interactive drill-downs, complexity visualisations, and test-to-code mapping.
OpenClover also offered test optimisation — tracking which tests exercise which source files and running only the affected tests after a change. This was ahead of its time.
The Problem
OpenClover is effectively dormant. The last release (4.5.0) was in October 2023. Its Gradle plugin hasn’t been updated since 2019. IDE plugins support IntelliJ up to 2016.3. Java support beyond version 9 is experimental, and there is no Java 21+ support. Kotlin is not supported at all.
Verdict
OpenClover’s ideas were excellent — source-level instrumentation and test optimisation remain compelling concepts. But the tool cannot be recommended for new projects. It doesn’t support modern Java, modern IDEs, or Kotlin. It’s included here for completeness and because teams maintaining legacy codebases may still encounter it.
Cobertura: A Historical Footnote
Cobertura uses offline bytecode instrumentation — it modifies compiled .class files on disk before tests run. It measures line and branch coverage and produces HTML and XML reports.
Cobertura’s lasting contribution is its XML report format, which became a de facto standard. GitLab CI, Codecov, and numerous other tools accept “Cobertura format” reports — often generated by other tools like JaCoCo or coverage.py.
The last release was version 2.1.1 in March 2015. It does not support modern Java, has no Kotlin support, and should not be used for new projects.
JCov: Oracle’s Internal Tool
JCov is Oracle’s coverage tool, used internally for OpenJDK development. It supports both offline and on-the-fly bytecode instrumentation and has one unique capability: it can instrument JDK bootstrap classes, which other tools cannot.
JCov also supports remote coverage data collection across multiple JVMs and per-test coverage tracking. However, it has no Gradle or Maven plugins, no IDE integration, and minimal documentation outside the OpenJDK wiki.
Unless you’re contributing to OpenJDK itself, JCov is not a practical choice.
IntelliJ IDEA Built-in Coverage
IntelliJ IDEA ships with its own coverage agent (intellij-coverage), which performs bytecode instrumentation at class load time. It’s what you use when you right-click a test in IntelliJ and select “Run with Coverage”.
The IntelliJ runner provides colour-coded gutter markers (green for covered, red for uncovered, yellow for partially covered branches) and, in the Ultimate edition, per-test coverage — click a line to see which tests executed it.
This is the same agent that Kover uses as its default backend. Within the IDE, it’s excellent for rapid feedback during development. But it produces .ic files in a proprietary format and isn’t designed for CI/CD pipelines. For build-integrated coverage, use JaCoCo or Kover instead.
Beyond Line Coverage: Mutation Testing with PIT
Traditional coverage has a fundamental blind spot: it measures execution, not verification. A test that calls a method and discards the result “covers” the same lines as one that asserts every output. Coverage tells you the code ran; it says nothing about whether your tests would catch a bug.
PIT (Pitest) addresses this through mutation testing. It automatically introduces small faults into your code — changing a > to >=, replacing a return value with null, removing a method call — and checks whether your test suite detects each mutation. If a test fails, the mutation is “killed”. If all tests still pass, the mutation “survived”, indicating a gap in test effectiveness.
plugins {
id("info.solidsoft.pitest") version "1.15.0"
}
pitest {
junit5PluginVersion.set("1.2.1")
targetClasses.set(listOf("com.example.service.*"))
mutators.set(listOf("DEFAULTS"))
outputFormats.set(listOf("HTML", "XML"))
}
Mutation testing is computationally expensive — it runs your test suite once per mutation — but it provides a genuinely meaningful quality signal. A test suite with 95% line coverage but 40% mutation score is telling you something important: the tests execute code without actually verifying behaviour.
PIT has dedicated Kotlin support via the pitest-kotlin plugin and integrates with both Gradle and Maven. For teams that are serious about test quality rather than coverage theatre, mutation testing is the next step after establishing baseline coverage.
Choosing the Right Tool
| JaCoCo | Kover | OpenClover | Cobertura | JCov | |
|---|---|---|---|---|---|
| Instrumentation | Bytecode (on-the-fly) | Delegates to IntelliJ/JaCoCo | Source-level | Bytecode (offline) | Bytecode (both) |
| Actively Maintained | Yes | Yes | Dormant | No (since 2015) | Niche |
| Kotlin Support | Partial (inline issues) | Excellent | None | None | Not tested |
| Gradle Plugin | Built-in | Official | Unmaintained | Third-party | None |
| Maven Plugin | Official | Official | Yes | Yes | None |
| CI/CD Ecosystem | Excellent | Good (JaCoCo-compatible) | Limited | Format only | None |
| Report Formats | HTML, XML, CSV | HTML, XML | HTML, XML, JSON, PDF | HTML, XML | HTML, XML |
Not a coverage tool — but worth pairing with one
| PIT (Pitest) | |
|---|---|
| Approach | Mutation testing (bytecode) |
| Actively Maintained | Yes |
| Kotlin Support | Via pitest-kotlin plugin |
| Gradle Plugin | Third-party (solidsoft) |
| Maven Plugin | Official |
| CI/CD Ecosystem | Good (HTML, XML) |
| Report Formats | HTML, XML, CSV |
For Java projects: JaCoCo remains the standard. Broad ecosystem support, minimal configuration, actively maintained.
For Kotlin or mixed projects: Kover with the IntelliJ backend. Correct handling of Kotlin constructs, with JaCoCo-compatible output for CI/CD integration.
For test quality validation: Add PIT/Pitest alongside your coverage tool. Coverage tells you what ran; mutation testing tells you whether your tests actually work.
Avoid for new projects: Cobertura (unmaintained since 2015), OpenClover (no modern Java/Kotlin support), JCov (no build tool integration).
Conclusion
Code coverage is a useful diagnostic, not a quality guarantee. The best test suite is one where every test exists because it guards against a specific, realistic failure — not because it inflates a percentage. As Fowler notes, the real indicators of test quality are whether bugs rarely escape to production and whether you can refactor with confidence.
Pick the coverage tool that fits your language and build system, set a reasonable threshold as a floor rather than a target, and invest your energy in writing tests that matter. A well-chosen 80% beats a forced 100% every time.
Further Reading
- Martin Fowler — Test Coverage — The definitive article on what coverage does and doesn’t tell you
- JaCoCo Documentation — Official JaCoCo reference
- Kover Gradle Plugin — Kover configuration and usage guide
- PIT Mutation Testing — Mutation testing for the JVM
- Google Research — Code Coverage at Google — Large-scale empirical study on coverage practices
- Kent Beck — Programmer Test Principles — Testing philosophy from the creator of TDD
Looking to improve your team’s testing strategy on the JVM? Get in touch for pragmatic advice on coverage tooling, test architecture, and quality engineering.
Back to Blog