Java · Comparison
JaCoCo vs SonarQube coverage: why the numbers differ
SonarQube imports JaCoCo's report and blends lines and branches into one number. One run gave 100% lines, 75% branches and 85.7% in SonarQube's formula.
Why do JaCoCo and SonarQube show different coverage?
SonarQube does not measure Java coverage. It imports JaCoCo’s XML report and computes its own number from it. Three things make that number differ:
- SonarQube’s “Coverage” mixes lines and branches into one figure.
- Files missing from the JaCoCo report count as uncovered in SonarQube.
- Exclusions are set separately in each tool.
One report, three percentages
JaCoCo’s report has a column per counter and no single total. SonarQube’s
headline number is, in its
metric definitions,
(CT + LC) / (B + EL): covered conditions plus covered lines, over all
conditions plus all lines to cover.
We ran JaCoCo 0.8.15 on one class with two tests:
| Number | Value |
|---|---|
| JaCoCo instructions | 8/8 (100%) |
| JaCoCo lines | 3/3 (100%) |
| JaCoCo branches | 3/4 (75%) |
| SonarQube’s formula on the same report | (3 + 3) / (4 + 3) = 85.7% |
All four describe the same run. The last row is the formula applied to the JaCoCo XML; we did not run SonarQube.
To compare like with like, open the Measures page in SonarQube. “Line coverage” matches JaCoCo’s lines, and “Condition coverage” matches JaCoCo’s branches.
Files missing from the report
A source file that SonarQube analyses but the imported report does not mention counts as uncovered: all its executable lines join the denominator. That happens when:
- a module’s report is not listed in
sonar.coverage.jacoco.xmlReportPaths; - a multi-module build has no aggregate report, so code tested from another module looks untested;
- the report on disk is older than the code being scanned.
Exclusions live in two places
JaCoCo’s excludes remove classes from JaCoCo’s report. SonarQube still
counts those files, as uncovered, unless you also list them in
sonar.coverage.exclusions. sonar.exclusions is different: it removes files
from the whole analysis, issues included.
Make the two agree
- Compare line coverage with lines, and condition coverage with branches.
- Generate the XML report in the same build, before the scan.
- Pass every module’s report in
sonar.coverage.jacoco.xmlReportPaths. - Mirror JaCoCo’s
excludesinsonar.coverage.exclusions.
What neither number tells you
Both describe a whole run. Neither says which test covered a line, or
whether each condition in a decision changed the result. In the class above,
if (signedIn && !expired) is never tested with an expired session.
npx supercov -- mvn test
npx supercov runs latest gaps
Supercov runs your Maven or Gradle command and records coverage per test, with
MC/DC. On that class it reported 3/3 lines, 4/4 branches and 1/2 MC/DC
conditions, and named !expired.
Choosing a Java coverage tool? See JaCoCo vs Cobertura.
Example evidence: fixture source, commands and recorded results (JSON). Measured October 12, 2026 with Supercov 4.3.0. These small examples are not a performance benchmark or a general conformance test.