skip to content

JaCoCo Aggregation

Merging coverage execution data from many modules into one project-wide report. Asked because per-module coverage numbers are misleading in a multi-module build.

on this pageshow

questions

6

Your testCodeCoverageReport runs successfully but the merged report shows 0% or is missing whole modules. How do you debug it?

level: middleimportance: must knowfreq 40%

answer

  1. module must apply jacoco -> exposes variant
  2. tests must run -> .exec non-empty
  3. reachable via jacocoAggregation/transitive
  4. testSuiteName must match producing suite
  5. check excludes; --info; log resolved inputs

basics

~10 s

Check that each module applies jacoco/jvm-test-suite (so it exposes coverage variants), that its tests actually ran and produced .exec data, that it's reachable via jacocoAggregation, and that the right testSuiteName is aggregated.

solid answer

~40 s

Work the resolution chain. (1) **Variant producers**: every module you expect must apply `jacoco` (and ideally `jvm-test-suite`); without it the module exposes no coverage variant and is silently skipped — explaining missing modules. (2) **Data exists**: tests must run and write `.exec`; if a module has no tests or they were `UP-TO-DATE` with no execution data, it contributes 0%. (3) **Reachability**: the module must be listed in `jacocoAggregation` or be a transitive project dependency of one. (4) **Suite match**: the report's `testSuiteName` must match the suite that produced the data (aggregating `test` when coverage came from an `integrationTest` suite yields empty). (5) **Excludes**: an over-broad `classDirectories` exclude filter can zero things out. Use `--info` and inspect the resolved `executionData`/`classDirectories` to see what was actually collected.

code

bash · 5 lines
bash
# See which test tasks actually ran for the aggregation
./gradlew :coverage:testCodeCoverageReport --info | grep -i test

# Confirm a module produced exec data
ls -la lib/build/jacoco/   # expect test.exec, non-empty

go deeper

for a junior

Check that the module applied jacoco and that its tests ran.

for a middle

Walk the producer/exec/reachability/suite/excludes pipeline and use --info plus inspecting resolved inputs.

for a senior

Reason about silent skipping of non-producer modules and suite/variant mismatches as the subtle failure modes.

for a principal

Add a convention plugin + CI check that asserts expected modules contribute, so silent gaps fail fast.

## Debug as a pipeline The merged report is `exec data x classes x sources`. A failure at any stage yields gaps. Walk them in order. ### 1. Is the module a coverage *producer*? The aggregation plugin only collects from projects that **expose** the coverage variants — which means they applied `jacoco` (directly, or via `jvm-test-suite` defaults). A module compiled but never given `jacoco` exposes nothing and is **silently skipped** (no error). Symptom: whole module absent. Fix: apply jacoco there. ### 2. Did tests actually run and emit .exec? Coverage data only exists if the test task executed and was instrumented. Causes of empty exec: - The module has no tests. - The test task was `NO-SOURCE`/skipped. - Instrumentation disabled. Symptom: module present but 0%. Check `build/jacoco/<suite>.exec` exists and is non-empty. ### 3. Is the module reachable from the aggregation set? It must be in `jacocoAggregation(project(...))` or pulled transitively. If you removed a dependency edge, a module can drop out. Symptom: previously-present module vanishes. ### 4. Does the suite name line up? If your coverage comes from a custom suite (e.g. `integrationTest`) but the report aggregates the default `test` suite, the report selects the wrong variant and looks empty. Match `testSuiteName` to the producing suite (or declare one report per suite). ### 5. Are excludes nuking the classes? A `classDirectories.setFrom(... exclude("**"))`-style mistake or an aggressive package exclude zeros coverage even when exec data is fine. ## Tools - `./gradlew :coverage:testCodeCoverageReport --info` — see which test tasks ran. - Inspect the resolved inputs: ```kotlin tasks.named<JacocoReport>("testCodeCoverageReport") { doFirst { logger.lifecycle("exec: ${executionData.files}") logger.lifecycle("classes: ${classDirectories.files}") } } ``` - Open the HTML report and confirm which packages appear; absent packages point back to step 1 or 3. ## Most common single cause In practice: a module that **didn't apply jacoco** (so it produced no `.exec` variant) — the report omits it without any warning.

  • A module compiles and has tests but is entirely absent from the report. Most likely cause?
    It didn't apply the `jacoco` (or jvm-test-suite) plugin, so it exposes no coverage variant and is silently skipped by aggregation.
  • The module appears but at 0% even though tests pass. What now?
    Check the `.exec` file is non-empty and that the report's `testSuiteName` matches the suite those tests belong to; mismatched suites select the wrong (empty) variant.

saying these in an interview costs you the question

  • Assuming aggregation errors out when a module lacks coverage — it skips silently, so absence is easy to miss.
  • Blaming the report renderer when the real issue is upstream (no jacoco plugin / wrong suite / empty exec).

context

open as a page

What does the jacoco-report-aggregation plugin do, and why would you use it in a multi-project Gradle build?

level: middleimportance: must knowfreq 55%

basics

~10 s

It merges JaCoCo coverage data from several subprojects into one combined HTML/XML report, so you see total coverage across the whole multi-project build instead of per-module reports.

open as a page

Walk me through setting up a dedicated aggregation/collector project for a multi-module repo, including which test suite it aggregates.

level: middleimportance: should knowfreq 35%

basics

~10 s

Create an empty module (e.g. :coverage), apply base and jacoco-report-aggregation, add jacocoAggregation(project(...)) for each module, optionally set testSuiteName, then run testCodeCoverageReport.

open as a page

How does jacoco-report-aggregation actually collect the .exec data, classes and sources from each subproject — what's the resolution mechanism?

level: seniorimportance: should knowfreq 30%

basics

~10 s

It uses Gradle's variant-aware dependency resolution: contributing projects publish coverage data, classes and sources as outgoing variants, and the aggregation plugin declares resolvable configurations that select each variant by attributes.

open as a page

Before jacoco-report-aggregation, teams hand-rolled a root JacocoReport that pointed at every module's exec/classes/sources. What are the concrete advantages of the plugin over that approach?

level: seniorimportance: should knowfreq 25%

basics

~10 s

The plugin resolves coverage via variants instead of hard-coded paths, so it's refactor-safe, auto-transitive, runs the right test tasks for you, and avoids cross-project configuration that breaks isolation and the configuration cache.

open as a page

How would you use jacoco-report-aggregation as the single source of truth for coverage across a large org's CI, and what are the trade-offs versus per-module enforcement?

level: principalimportance: nice to knowfreq 15%

basics

~20 s

Standardize a convention plugin that adds a code-free aggregation module, have CI run one testCodeCoverageReport and publish its XML to the coverage service. Aggregated gives a true cross-module number but can mask weak modules; pair it with per-module rules for accountability.

open as a page