How do you wire JaCoCo into a Gradle or Maven build to generate a coverage report?
answer
- Two jobs: attach agent (prepare-agent) + generate report
- Gradle: built-in 'jacoco' plugin, jacocoTestReport task
- Maven: jacoco-maven-plugin, prepare-agent + report goals
- Maven argLine trap: keep @{argLine} or agent vanishes silently
- One exec per JVM → merge forked/integration/multi-module data; enable XML for CI
basics
~20 sAdd the JaCoCo plugin to your build. It attaches an agent to the test run so coverage is recorded, then a report task turns that data into an HTML report. In Gradle you apply the jacoco plugin and run jacocoTestReport; in Maven you bind the prepare-agent and report goals.
solid answer
~40 sBoth build tools have first-class JaCoCo support. In **Gradle**, applying the built-in `jacoco` plugin adds the agent to the `test` task automatically and registers a `jacocoTestReport` task; you typically make the report depend on `test` and run `./gradlew test jacocoTestReport`, getting HTML/XML under `build/reports/jacoco`. In **Maven**, you add the `jacoco-maven-plugin` and bind two goals: `prepare-agent` (sets the `argLine` so Surefire launches the test JVM with the JaCoCo agent) and `report` (renders from `target/jacoco.exec`), usually in the `test`/`verify` phases. Key subtleties: the agent only covers code run in *that* JVM, so forked tests or separate integration-test JVMs need their own exec files (then merged); and in Maven, if you set `argLine` yourself you must include `@{argLine}` or you'll silently drop the agent. The XML report is the format SonarQube and CI dashboards ingest.
code
java · 19 lines// build.gradle (Groovy DSL)
plugins {
id 'java'
id 'jacoco'
}
test {
finalizedBy jacocoTestReport // always report after testing
}
jacocoTestReport {
dependsOn test
reports {
xml.required = true // CI / SonarQube ingest this
html.required = true // humans browse this
}
}
// Run: ./gradlew test jacocoTestReport
// HTML: build/reports/jacoco/test/html/index.htmlgo deeper
Can apply the Gradle jacoco plugin (or Maven plugin) and run the report task to get an HTML report.
Understands prepare-agent vs report, where outputs land, and enabling XML for CI; knows finalizedBy/dependsOn wiring.
Handles forked/integration/multi-module merging, the Maven argLine @{} gotcha, and aggregate reports across modules.
Designs the org's coverage pipeline (aggregation, CI gates, Sonar integration), and standardizes exec-file collection/merge so distributed and integration coverage isn't lost.
## The two jobs JaCoCo must do in a build Whatever the build tool, integrating JaCoCo means accomplishing two things: 1. **Attach the agent** to the JVM that runs your tests, so probes record execution into a `*.exec` file. 2. **Generate a report** from that `*.exec` file plus the compiled classes and sources. Build plugins automate both. ## Gradle Gradle ships a built-in **`jacoco` plugin**. Applying it: ```groovy plugins { id 'java'; id 'jacoco' } test { finalizedBy jacocoTestReport } // run the report after tests jacocoTestReport { dependsOn test reports { xml.required = true // for CI / SonarQube html.required = true // for humans } } ``` - Applying the plugin automatically adds the JaCoCo **agent** to the `test` task — you don't configure `-javaagent` by hand. - It registers a **`jacocoTestReport`** task that reads `build/jacoco/test.exec` and writes reports to `build/reports/jacoco/test/`. - Run `./gradlew test jacocoTestReport`. (`finalizedBy`/`dependsOn` wires the order so the report always reflects the latest run.) - A second task, **`jacocoTestCoverageVerification`**, enforces thresholds (covered in a separate question). ## Maven Maven uses the **`jacoco-maven-plugin`** with **executions** bound to lifecycle phases: ```xml <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <executions> <execution> <id>prepare-agent</id> <goals><goal>prepare-agent</goal></goals> </execution> <execution> <id>report</id> <phase>test</phase> <goals><goal>report</goal></goals> </execution> </executions> </plugin> ``` - **`prepare-agent`** doesn't run tests itself; it sets a property (by default **`argLine`**) containing the `-javaagent:...` option. The Surefire plugin (which runs unit tests) reads `argLine` and passes it to the forked test JVM, so the agent is attached. - **`report`** renders from `target/jacoco.exec` into `target/site/jacoco/`. - For integration tests run by Failsafe, use the analogous `prepare-agent-integration` / `report-integration` goals and the `verify` phase. ## The `argLine` gotcha (Maven) If you *also* set `<argLine>` for Surefire yourself (e.g. memory flags), you **overwrite** the value JaCoCo set unless you reference it: write `<argLine>@{argLine} -Xmx512m</argLine>`. The `@{...}` late-evaluation syntax preserves JaCoCo's injected agent option. Forgetting it makes the agent silently disappear and coverage reads 0% with no error — a classic confusing failure. ## One exec file per JVM The agent only records code executed in the JVM it's attached to. Consequences: - **Forked tests** (`forkEvery`, parallel forks) each produce data that the plugin appends/merges into the exec file — usually handled automatically. - **Separate integration-test JVMs**, or tests that launch the app in another process, produce a *separate* exec file. You must collect each and **merge** them (Gradle `JacocoMerge` / `executionData` from multiple tasks; Maven `merge` goal) before reporting, or that code shows as uncovered. - **Multi-module** projects need an aggregate report (Gradle `jacoco-report-aggregation` plugin; Maven `report-aggregate`) to combine modules. ## Outputs and consumers - **HTML** — browse `index.html` for the colored source view. - **XML** — the machine-readable form consumed by **SonarQube**, Codecov, and CI quality gates. Always enable it in CI. - **CSV** — simple tabular export. ## Mental checklist Agent attached to the right JVM(s) → exec file(s) produced → merged if multiple → report generated with classes + sources → XML enabled for CI.
- Your Maven coverage suddenly drops to 0% after someone added a memory flag. What's the likely cause?They set `<argLine>` for Surefire and overwrote JaCoCo's injected agent option. They must reference the existing value with `@{argLine}` (e.g. `@{argLine} -Xmx512m`) so the prepare-agent goal's `-javaagent` is preserved.
- Integration tests pass but the code they exercise shows as uncovered. Why?Those tests likely run in a separate JVM/process whose execution data went to a different exec file (or none was captured). You need to attach the agent to that JVM, collect its exec file, and merge it into the report — otherwise that JVM's coverage is invisible.
saying these in an interview costs you the question
- Manually passing -javaagent when the plugin already adds it (double instrumentation).
- In Maven, overwriting argLine without @{argLine}, silently disabling the agent.
- Expecting coverage from a separately launched JVM without capturing/merging its exec file.
- Forgetting to enable the XML report, then wondering why SonarQube shows no coverage.