jacoco-analysis

Use whenever a Java, Kotlin, Scala or Groovy project on Gradle or Maven needs code coverage or a coverage gate: JaCoCo agent setup, HTML, XML and CSV reports, check rules that fail the build below a minimum such as 80% lines, the six counters, and JaCoCo XML to LCOV or Cobertura.

/plugin install qa-test-reporting@testland-qa

First time in Claude Code? Add the marketplace with /plugin marketplace add testland/qa

View source (opens in a new tab)

Overview

JaCoCo (the canonical JVM coverage tool) instruments Java bytecode at runtime via a Java agent, then aggregates execution data into per-class / per-method / per-line reports.

Per jacoco-counters (opens in new window), JaCoCo measures six counters:

CounterDefinition
Instructions (C0)"The smallest unit JaCoCo counts are single Java byte code instructions."
Branches (C1)Coverage "for all if and switch statements."
Lines"A source line is considered executed when at least one instruction that is assigned to this line has been executed."
Methods"A method is considered as executed when at least one instruction has been executed."
Classes"A class is considered as executed when at least one of its methods has been executed."
Cyclomatic Complexity"v(G) = B - D + 1 (branches minus decision points plus one)" - minimum number of paths needed to cover all paths through a method.

This skill covers Maven (the primary deployment), with the bytecode-only key insight: "All counters work at bytecode level, making them available regardless of debug information presence" (jacoco-counters (opens in new window)). Gradle, the XML parser, and the cross-language conversions live in the reference file.

When to use

  • A JVM project (Java / Kotlin / Scala / Groovy) needs coverage and the team is on Maven or Gradle.
  • A Cobertura-based pipeline is migrating to JaCoCo (typical: Cobertura is unmaintained for JDK 11+; JaCoCo handles modern bytecode).
  • Cross-language aggregation needs JaCoCo XML converted to LCOV / Cobertura.

How to use

  1. Confirm the JVM build is Maven / Gradle (see When to use).
  2. Wire the jacoco-maven-plugin with the three primary goals - prepare-agent, report, check (below); Gradle's equivalent is in references/gradle-parsing-and-ci.md.
  3. Run mvn verify; read the check verdict and the HTML at target/site/jacoco/index.html.
  4. Gate on INSTRUCTION + BRANCH ratios, then parse jacoco.xml for PR comments and convert to LCOV / Cobertura for cross-language aggregation - the parser, the counter-aware gating table, the format conversions, and the full Maven verify CI job are in references/gradle-parsing-and-ci.md.

Wire the Maven plugin

Per jacoco-maven (opens in new window), the plugin defines several goals; the three primary ones are prepare-agent, report, and check (jacoco-maven (opens in new window)):

<plugin>
  <groupId>org.jacoco</groupId>
  <artifactId>jacoco-maven-plugin</artifactId>
  <version>0.8.13</version>
  <executions>
    <execution>
      <id>prepare-agent</id>
      <goals>
        <goal>prepare-agent</goal>
      </goals>
    </execution>
    <execution>
      <id>report</id>
      <phase>verify</phase>
      <goals>
        <goal>report</goal>
      </goals>
    </execution>
    <execution>
      <id>check</id>
      <phase>verify</phase>
      <goals>
        <goal>check</goal>
      </goals>
      <configuration>
        <rules>
          <rule>
            <element>BUNDLE</element>
            <limits>
              <limit>
                <counter>INSTRUCTION</counter>
                <value>COVEREDRATIO</value>
                <minimum>0.80</minimum>
              </limit>
              <limit>
                <counter>BRANCH</counter>
                <value>COVEREDRATIO</value>
                <minimum>0.70</minimum>
              </limit>
            </limits>
          </rule>
        </rules>
      </configuration>
    </execution>
  </executions>
</plugin>

prepare-agent configures the JaCoCo runtime agent for the forked test JVM; report generates HTML / XML / CSV from target/jacoco.exec; check enforces thresholds.

Per jacoco-maven (opens in new window): "Maven 3.0 or higher and Java 1.8 or higher for the Maven runtime, Java 1.5 or higher for the test executor."

Report formats

The report goal emits three artifacts under target/site/jacoco/:

FileFormatUse
index.html + assetsHTML drill-downHuman review.
jacoco.xmlXML (with DTD)CI parsing; Cobertura-shape conversion.
jacoco.csvCSV per-classSpreadsheets; quick scripting.

For Maven multi-module projects, the per-module reports go under each module's target/site/jacoco/. For an aggregated report, use the report-aggregate goal in a parent module.

Gating with the check goal

The check goal accepts a list of rules; each rule scopes by element (BUNDLE / PACKAGE / CLASS / SOURCEFILE / METHOD) and constrains via limits with counter / value / minimum or maximum:

<rules>
  <rule>
    <element>BUNDLE</element>
    <limits>
      <limit>
        <counter>INSTRUCTION</counter>
        <value>COVEREDRATIO</value>
        <minimum>0.80</minimum>
      </limit>
    </limits>
  </rule>
  <rule>
    <element>CLASS</element>
    <excludes>
      <exclude>*Test</exclude>
      <exclude>*IT</exclude>
    </excludes>
    <limits>
      <limit>
        <counter>METHOD</counter>
        <value>MISSEDCOUNT</value>
        <maximum>0</maximum>
      </limit>
    </limits>
  </rule>
</rules>

The two-rule pattern: a BUNDLE (whole-project) floor + a per-CLASS strict rule for production code (excluding tests).

value options (per the JaCoCo Maven plugin's check-mojo docs):

valueMeaning
COVEREDRATIOHit / total (0.0 to 1.0).
MISSEDRATIOMissed / total (0.0 to 1.0).
COVEREDCOUNTAbsolute number hit.
MISSEDCOUNTAbsolute number missed.
TOTALCOUNTAbsolute total.

minimum floors the value; maximum caps it. Counter options are INSTRUCTION / BRANCH / LINE / METHOD / CLASS / COMPLEXITY (per jacoco-counters (opens in new window)). Gate on INSTRUCTION for the whole-repo floor and BRANCH separately, with a bounded METHOD miss:

<limit><counter>INSTRUCTION</counter><value>COVEREDRATIO</value><minimum>0.80</minimum></limit>
<limit><counter>BRANCH</counter><value>COVEREDRATIO</value><minimum>0.70</minimum></limit>
<limit><counter>METHOD</counter><value>MISSEDCOUNT</value><maximum>5</maximum></limit>

Whole-bundle 80% instruction; 70% branch; allow up to 5 untested methods. The per-counter "when to gate on which" table is in references/gradle-parsing-and-ci.md.

Worked example

A Maven service wiring JaCoCo for the first time:

  1. Add the plugin block above to pom.xml (all three executions: prepare-agent, report, check).
  2. Run the build:
./mvnw -B verify

prepare-agent instruments the forked test JVM; the tests write target/jacoco.exec; report renders target/site/jacoco/index.html; check evaluates the rules.

If instruction coverage lands at 82% but branch coverage is 66%, the build fails on the BRANCH rule (minimum 0.70), naming the bundle and the missed limit. Open target/site/jacoco/index.html, drill into the red (missed) branches, add the tests that exercise them, and re-run ./mvnw -B verify until check passes.

Operating in CI

Run ./mvnw -B verify so check gates the build, then upload target/site/jacoco/ with if: always() (coverage matters most on runs that failed the gate). For a coverage SaaS or cross-language aggregation, convert jacoco.xml to LCOV in the same job and upload that. The full GitHub Actions workflow (verify, artifact upload, xml2lcov conversion, SaaS upload) is in references/gradle-parsing-and-ci.md.

Anti-patterns

Anti-patternWhy it failsFix
Skipping prepare-agentTests run without instrumentation; coverage data empty.Always include prepare-agent (see Wire the Maven plugin).
Whole-project rule with INSTRUCTION onlyBranch regressions invisible - line% looks fine while branch% drops.Separate INSTRUCTION + BRANCH rules (see Gating with the check goal).
<element>BUNDLE</element> on every rulePer-class violations bury inside an aggregate that passes.Add a per-CLASS rule with excludes for tests (see Gating with the check goal).
Using JaCoCo report HTML as PR-comment inputHTML is for humans; PR comments need machine-readable.Parse jacoco.xml (see references); generate the comment from there.
Running JaCoCo on test code"Coverage" of tests is meaningless and inflates aggregate.excludes for *Test, *IT, *Spec (see Gating with the check goal).
Aggregating LINE coverage when methods are shortInflated; the coarse-grained measure looks fine.Pair LINE with BRANCH always (see the counter-aware table in references).
Failing the build on COMPLEXITY without a tested floorForces refactors of legitimately complex code (algorithms).Use COMPLEXITY as informational; gate on INSTRUCTION + BRANCH.

Limitations

  • Bytecode-level only. Per jacoco-counters (opens in new window), all counters work at bytecode level - useful for source-format independence but means kotlinc / scalac inlining can produce surprising line-coverage shapes.
  • Per-method desc= is JVM signature. Cross-source-language reports (Kotlin generates JVM methods) need the source file as the pivot, not the method descriptor.
  • prepare-agent only instruments forked test JVMs. Surefire's forkCount=0 (run-in-Maven-JVM mode) bypasses the agent; coverage data is empty. Use the default fork mode.
  • No PR-context awareness. Pair with coverage-diff-reporter for the diff vs main.

References

  • jacoco-counters (opens in new window) - six counters (instructions, branches, lines, methods, classes, cyclomatic complexity), bytecode-level measurement, formula for cyclomatic complexity.
  • jacoco-maven (opens in new window) - prepare-agent / report / check goals; Maven + JVM version requirements; HTML report output path.
  • references/gradle-parsing-and-ci.md - the Gradle plugin config, the jacoco.xml parser, the counter-aware gating table, JaCoCo-to-Cobertura / LCOV conversion, and the Maven verify CI job.
  • lcov-analysis (references/cobertura.md) - sister parser; JaCoCo XML can convert to Cobertura XML for sibling tooling.
  • lcov-analysis - sister parser; JaCoCo can convert to LCOV for cross-language aggregation.
  • coverage-diff-reporter, test-coverage-targeter - downstream skills consuming the parsed JaCoCo output.
Reference

JaCoCo Gradle config, XML parsing, counter-aware gating, and CI

View source (opens in a new tab) (opens in new window)

Deep reference for the jacoco-analysis SKILL.md. Consult for the Gradle plugin equivalent, the jacoco.xml parser, the per-counter gating table, the JaCoCo-to-Cobertura / LCOV conversions, and the Maven verify CI job.

Gradle equivalent

plugins {
  id 'java'
  id 'jacoco'
}

jacoco {
  toolVersion = '0.8.13'
}

test {
  finalizedBy jacocoTestReport
}

jacocoTestReport {
  dependsOn test
  reports {
    xml.required = true
    html.required = true
    csv.required = false
  }
}

jacocoTestCoverageVerification {
  violationRules {
    rule {
      limit {
        counter = 'INSTRUCTION'
        value = 'COVEREDRATIO'
        minimum = 0.80
      }
      limit {
        counter = 'BRANCH'
        value = 'COVEREDRATIO'
        minimum = 0.70
      }
    }
  }
}

check.dependsOn jacocoTestCoverageVerification

Parse jacoco.xml

The JaCoCo XML format mirrors the report tree (report -> package -> class -> method -> counter):

<report name="my-app">
  <package name="com/example/checkout">
    <class name="com/example/checkout/Cart" sourcefilename="Cart.java">
      <method name="addItem" desc="(LItem;)V" line="12">
        <counter type="INSTRUCTION" missed="0" covered="15"/>
        <counter type="BRANCH" missed="1" covered="3"/>
        <counter type="LINE" missed="0" covered="3"/>
        <counter type="METHOD" missed="0" covered="1"/>
        <counter type="COMPLEXITY" missed="0" covered="2"/>
      </method>
      <counter type="INSTRUCTION" missed="0" covered="42"/>
      ...
    </class>
    <counter type="INSTRUCTION" missed="0" covered="100"/>
    ...
  </package>
  <counter type="INSTRUCTION" missed="20" covered="500"/>
  ...
</report>
# scripts/parse_jacoco.py
import xml.etree.ElementTree as ET

def parse_jacoco(path):
    root = ET.parse(path).getroot()
    files = []
    for pkg in root.findall('package'):
        for cls in pkg.findall('class'):
            counters = {c.get('type'): {
                            'missed': int(c.get('missed')),
                            'covered': int(c.get('covered')),
                        } for c in cls.findall('counter')}
            files.append({
                'package': pkg.get('name'),
                'name': cls.get('name'),
                'sourcefile': cls.get('sourcefilename'),
                'counters': counters,
            })
    return files

Counter-aware metrics

Per jacoco-counters (opens in new window), the six counters have different semantics. Don't aggregate naively:

CounterWhen to gate on it
INSTRUCTIONThe most granular; least sensitive to source formatting. Best whole-repo gate.
LINEMost intuitive for reviewers; aggregates per source line.
BRANCHCritical for control flow correctness; gate separately from line.
METHODCoarse-grained; "any test touched this method" - useful as a "no dead code" floor.
CLASSEven coarser; "any test touched this class" - proves the test suite at least loads it.
COMPLEXITYPair with branch coverage; high-complexity uncovered methods are the highest-risk.

Cross-language: convert JaCoCo XML

For projects that mix JVM with other languages and want one coverage UI, convert JaCoCo XML to a sibling format.

To Cobertura

# Use the cover2cover.py script (community-maintained):
python cover2cover.py target/site/jacoco/jacoco.xml src/main/java > target/cobertura.xml

Then feed lcov-analysis (references/cobertura.md).

To LCOV

# Use the xml2lcov converter (per LCOV's language-agnostic converter family):
xml2lcov target/site/jacoco/jacoco.xml > target/jacoco.info

Then feed lcov-analysis. LCOV is the language-agnostic interchange format; LCOV's own documentation lists JaCoCo conversion as a supported path.

CI shape

- name: Run Maven verify
  run: ./mvnw -B verify

- name: Upload JaCoCo report
  if: always()
  uses: actions/upload-artifact@v4
  with:
    name: jacoco
    path: target/site/jacoco/

- name: Convert to LCOV for SaaS
  run: xml2lcov target/site/jacoco/jacoco.xml > target/jacoco.info

- name: Upload to coverage SaaS
  uses: codecov/codecov-action@v5
  with:
    files: target/jacoco.info