Writing tests is only the first step. Knowing how much of your code those tests actually exercise — and which parts remain untested — requires a code coverage tool. JaCoCo (Java Code Coverage) is the de facto standard for Java projects and integrates seamlessly with JUnit 6, Maven, Gradle, and CI pipelines. This guide walks you through the complete JaCoCo setup, report interpretation, and coverage enforcement with real configuration examples.
What JaCoCo Measures
| Metric | What it counts | When it matters |
|---|---|---|
| Line coverage | Executable source lines touched by tests | Basic coverage baseline |
| Branch coverage | if/else, switch, ternary branches taken | Logic-heavy code |
| Method coverage | Methods called at least once | Dead code detection |
| Class coverage | Classes instantiated at least once | Unused class detection |
| Instruction coverage | JVM bytecode instructions executed | Most granular metric |
| Complexity coverage | Cyclomatic paths through code | Complex business logic |
Maven Setup: JaCoCo Plugin
<build>
<plugins>
<!-- JaCoCo Maven Plugin -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<executions>
<!-- Execution 1: prepare-agent
Attaches JaCoCo Java agent BEFORE tests run.
The agent instruments bytecode at runtime to track coverage. -->
<execution>
<id>prepare-agent</id>
<goals><goal>prepare-agent</goal></goals>
</execution>
<!-- Execution 2: report
Generates HTML, XML, and CSV reports AFTER tests complete.
Output: target/site/jacoco/index.html -->
<execution>
<id>generate-report</id>
<phase>verify</phase>
<goals><goal>report</goal></goals>
</execution>
<!-- Execution 3: check
Enforces minimum coverage thresholds.
Fails the build if coverage drops below the defined rules. -->
<execution>
<id>enforce-coverage</id>
<phase>verify</phase>
<goals><goal>check</goal></goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element> <!-- entire project -->
<limits>
<!-- Require at least 80% line coverage -->
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
<!-- Require at least 70% branch coverage -->
<limit>
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>0.70</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Gradle Setup: JaCoCo Plugin
// build.gradle
plugins {
id 'java'
id 'jacoco' // built-in Gradle plugin — no version needed
}
jacoco {
// Pin the JaCoCo tool version for reproducible builds
toolVersion = "0.8.12"
}
test {
useJUnitPlatform()
// JaCoCo hooks into the test task automatically when the plugin is applied
finalizedBy jacocoTestReport // generate report immediately after tests
}
jacocoTestReport {
dependsOn test // ensure tests run before report generation
reports {
xml.required = true // required for CI tools (Codecov, SonarQube)
html.required = true // human-readable report
csv.required = false // optional
}
// Exclude generated code, DTOs, and configuration classes from coverage
afterEvaluate {
classDirectories.setFrom(files(classDirectories.files.collect {
fileTree(dir: it, excludes: [
'**/config/**',
'**/dto/**',
'**/generated/**',
'**/*Application*'
])
}))
}
}
jacocoTestCoverageVerification {
violationRules {
rule {
// Enforce minimum 80% line coverage on the entire project
limit {
counter = 'LINE'
value = 'COVEREDRATIO'
minimum = 0.80
}
}
rule {
// Enforce per-class: each class must have 70%+ branch coverage
element = 'CLASS'
limit {
counter = 'BRANCH'
value = 'COVEREDRATIO'
minimum = 0.70
}
// Exclude trivial classes from per-class rule
excludes = ['com.example.dto.*', 'com.example.config.*']
}
}
}
// Run coverage verification as part of the 'check' lifecycle
check.dependsOn jacocoTestCoverageVerification
Running Coverage from the Command Line
# Maven: run tests + generate report + enforce thresholds
mvn verify
# Reports generated at: target/site/jacoco/index.html
# Maven: generate report without enforcing thresholds
mvn test jacoco:report
# Gradle: run tests + generate report
./gradlew test jacocoTestReport
# Reports generated at: build/reports/jacoco/test/html/index.html
# Gradle: run tests + enforce thresholds
./gradlew test jacocoTestCoverageVerification
Reading the JaCoCo HTML Report
JaCoCo Report — Summary View
Element Missed Instructions Cov. Missed Branches Cov.
---------------------------------------------------------------------------
com.example.service 24 of 312 92% 8 of 48 83%
com.example.repository 12 of 98 88% 4 of 22 82%
com.example.controller 6 of 145 96% 2 of 18 89%
In the source view:
Green = covered (executed at least once)
Red = not covered (never executed)
Yellow = partially covered branch (one branch taken, other not)
Excluding Classes from Coverage
<!-- Maven: exclude classes from JaCoCo report -->
<configuration>
<excludes>
<!-- Exclude Spring Boot main application class -->
<exclude>com/example/Application.class</exclude>
<!-- Exclude all DTO classes (just data containers) -->
<exclude>com/example/dto/**/*.class</exclude>
<!-- Exclude generated Mapstruct mappers -->
<exclude>com/example/mapper/**/*Impl.class</exclude>
<!-- Exclude JPA entity classes -->
<exclude>com/example/entity/**/*.class</exclude>
</excludes>
</configuration>
<!-- Or use @Generated annotation on classes to exclude them automatically -->
<!-- JaCoCo 0.8.2+ excludes classes annotated with @Generated -->
Frequently Asked Questions (FAQs)
Q1: What is a good code coverage target for a Java project?
There is no universal answer, but 70–80% line coverage is a widely cited practical target for business logic. Coverage above 90% often means you are writing tests for getters/setters and configuration classes rather than meaningful behaviour. More important than the number is branch coverage on complex logic — aim for 80%+ branch coverage on service and domain classes while accepting lower numbers for infrastructure code, DTOs, and configuration.
Q2: Why does JaCoCo show 0% coverage even though my tests are passing?
This typically means the JaCoCo agent was not attached when tests ran. For Maven, make sure the prepare-agent execution runs before the test phase (it binds to the initialize phase by default). For Gradle, make sure the jacoco plugin is applied. Also check that you are calling mvn verify (not just mvn test) because report and check goals bind to the verify phase.
Q3: Can I get separate coverage reports for unit and integration tests?
Yes. Configure two separate JaCoCo executions with different data file destinations using <destFile>target/jacoco-unit.exec</destFile> and <destFile>target/jacoco-it.exec</destFile>. Then use the merge goal to combine them for the overall project report, while keeping individual reports for unit and integration coverage separately. This is useful for showing that integration tests cover different paths than unit tests.
Q4: How do I integrate JaCoCo with SonarQube?
SonarQube reads JaCoCo’s XML report (not the HTML report). Ensure jacocoTestReport generates XML (xml.required = true in Gradle, or the XML format is included by default in Maven). Then configure the Sonar scanner to find the report: sonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml for Maven, or the equivalent Gradle property. SonarQube then displays coverage inline in its code browser.
Q5: Does high code coverage mean my code is well-tested?
No. Coverage measures whether lines were executed, not whether they were correctly verified. A test that calls a method but has no assertions can achieve 100% line coverage while testing nothing. Coverage is a useful lower bound — 0% covered means definitely not tested; 80% covered means at least some testing occurred. Combine coverage metrics with mutation testing (see Mutation Testing with PIT and JUnit 6) for a true measure of test quality.
See Also
- Running JUnit 6 Tests in CI/CD Pipelines (GitHub Actions, Jenkins)
- Test Reporting in JUnit 6: Allure vs Surefire vs Custom Reports
- Mutation Testing with PIT and JUnit 6 (Improve Test Quality)
- Tags and Test Suites in JUnit 6: Organizing Large Test Bases
- JUnit 6 Tutorial: Complete Series Index
Conclusion
JaCoCo gives you visibility into what your tests actually exercise. Configure it once with Maven or Gradle, set sensible coverage thresholds (80% line, 70% branch for business logic), exclude boilerplate classes, and integrate the check into your CI pipeline. Used consistently, JaCoCo turns code coverage from a vanity metric into a genuine quality gate that catches gaps before they reach production.
Next: Test Reporting in JUnit 6: Allure vs Surefire vs Custom Reports — go beyond raw XML and build test reports that stakeholders can actually read and act on.
No Comments yet!