Skip to main content

Code Coverage with JaCoCo and JUnit 6: Complete Setup

A complete JaCoCo setup guide for JUnit 6 projects. Covers Maven and Gradle plugin configuration, coverage metrics explained, HTML report interpretation, enforcing minimum thresholds in CI, excluding generated code, and integrating with SonarQube.

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

MetricWhat it countsWhen it matters
Line coverageExecutable source lines touched by testsBasic coverage baseline
Branch coverageif/else, switch, ternary branches takenLogic-heavy code
Method coverageMethods called at least onceDead code detection
Class coverageClasses instantiated at least onceUnused class detection
Instruction coverageJVM bytecode instructions executedMost granular metric
Complexity coverageCyclomatic paths through codeComplex 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

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!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.