skip to content

Coverage & Plugin Testing

Measuring coverage with JaCoCo, enforcing thresholds, aggregating across modules, and functional-testing plugins with TestKit. Interviewers use this area to check whether your quality signals are enforced or merely decorative.

on this pageshow

explore

questions

21

How do you enable JaCoCo code-coverage reporting in a Gradle build, and what does applying the jacoco plugin give you?

level: juniorimportance: must knowfreq 70%

answer

  1. core plugin, apply by id
  2. no dependency needed
  3. agent attaches to Test task
  4. writes build/jacoco/test.exec
  5. registers jacocoTestReport

basics

~10 s

Add jacoco to the plugins {} block. Gradle then adds a jacocoTestReport task and a jacoco {} extension, and instruments your tests so coverage data is collected when you run test.

solid answer

~40 s

JaCoCo is enabled by applying Gradle's built-in `jacoco` plugin in the `plugins {}` block — no external dependency or version is needed because it ships with Gradle. Applying it does three things: it adds the `jacoco {}` extension (where you can set `toolVersion`), it registers a `JacocoReport` task named `jacocoTestReport` for the `test` task, and it configures the `test` task so the JVM runs with the JaCoCo agent attached, writing execution data to `build/jacoco/test.exec`. The report task is *not* part of `check` by default and does not run automatically when you run `test`; you invoke `./gradlew test jacocoTestReport` or wire a dependency. You typically also pin `toolVersion` so the agent matches your JDK bytecode level.

code

kotlin · 10 lines
kotlin
plugins {
    java
    jacoco
}

jacoco {
    toolVersion = "0.8.12"
}

// Run: ./gradlew test jacocoTestReport

go deeper

for a junior

Know that you add jacoco to the plugins block and run jacocoTestReport to get coverage.

for a middle

Explain the agent/exec-data vs report split and that the report task isn't run by test automatically.

for a senior

Discuss toolVersion pinning against JDK bytecode levels and where outputs land for CI consumption.

for a principal

Frame coverage as a build-policy concern: standardizing the plugin + version across modules via a convention plugin so every team reports consistently.

## What JaCoCo is JaCoCo ("Java Code Coverage") is a library that measures which lines, branches, and instructions of your code were exercised while tests ran. It works by **instrumenting** bytecode on the fly via a Java agent: when the test JVM starts, the agent records every executed instruction into a binary **execution-data file** (an `.exec` file). A separate **report** step reads that `.exec` file plus your compiled `.class` files and source files to produce human- and machine-readable coverage reports. ## The Gradle `jacoco` plugin Gradle ships a core plugin you apply by id — there is no Maven-style dependency to declare: ```kotlin plugins { java jacoco } ``` Applying it performs three jobs: 1. **Adds the `jacoco {}` extension** of type `JacocoPluginExtension`, where you configure the agent/tooling: most commonly `toolVersion = "0.8.12"` and optionally `reportsDirectory`. 2. **Configures every `Test` task** so the JaCoCo agent is attached. After a test run, execution data lands in `build/jacoco/<testTaskName>.exec` (e.g. `build/jacoco/test.exec`). 3. **Registers a `jacocoTestReport` task** of type `JacocoReport`, pre-wired to read the `test` task's execution data and the main source set's classes/sources. ## Pinning the tool version The JaCoCo agent must understand the bytecode your JDK produces. With newer JDKs you frequently bump `toolVersion` to a release that supports that class-file major version, otherwise instrumentation fails with an "Unsupported class file major version" error. Set it once in the extension: ```kotlin jacoco { toolVersion = "0.8.12" } ``` ## It does not run by itself A common surprise: `jacocoTestReport` is **not** a dependency of `check` and `test` does **not** trigger it. Running `./gradlew test` produces only the `.exec` data; you must also run the report task (or wire it — covered separately). The report task is also `UP-TO-DATE`/cacheable based on its inputs, so re-running without new test execution data skips work. ## Where outputs land - Execution data: `build/jacoco/test.exec` - HTML report (when enabled): `build/reports/jacoco/test/html/index.html` - XML report (when enabled): `build/reports/jacoco/test/jacocoTestReport.xml`

  • Does running `./gradlew test` produce a coverage report?
    No. The `test` run only writes execution data to `build/jacoco/test.exec`. You must also run `jacocoTestReport` (or wire it as a finalizer/dependency) to render HTML/XML.
  • Why might you need to set `toolVersion`?
    The bundled JaCoCo version may not support the class-file major version emitted by a newer JDK, causing instrumentation failures. Pinning a newer `toolVersion` fixes it.

saying these in an interview costs you the question

  • Claiming you must add a `jacoco` dependency in `dependencies {}` — it is a core plugin applied by id.
  • Assuming the report is generated automatically by the `test` task.

context

open as a page

What is Gradle TestKit's GradleRunner, and how do you use it to functionally test a Gradle plugin?

level: juniorimportance: must knowfreq 55%

basics

~20 s

GradleRunner is the TestKit entry point that runs a real Gradle build against a temporary project directory. You create it, point it at a project dir, pass arguments, and call build() to execute and inspect the result.

open as a page

What does the jacocoTestCoverageVerification task do in a Gradle build, and how do you make it fail the build when coverage drops below a threshold?

level: juniorimportance: must knowfreq 60%

basics

~10 s

It's a JaCoCo plugin task that checks coverage against rules you define in violationRules. If coverage is below the configured minimum, the task fails, which fails the build.

open as a page

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%

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.

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

How do you configure the jacocoTestReport task to produce an XML report for CI and an HTML report for humans?

level: middleimportance: must knowfreq 65%

basics

~10 s

In the jacocoTestReport task, set reports { xml.required.set(true); html.required.set(true) }. XML is for tools like SonarQube/Codecov; HTML is the browsable report. You can also turn off csv.

open as a page

By default jacocoTestReport doesn't run when you run tests. How do you wire it so the coverage report is produced right after the test task?

level: middleimportance: must knowfreq 60%

basics

~10 s

Make the report run with tests: tasks.test { finalizedBy(tasks.jacocoTestReport) }, and have the report depend on tests with tasks.jacocoTestReport { dependsOn(tasks.test) }. Then ./gradlew test produces the report automatically.

open as a page

How do you assert on the result of a TestKit build — what does BuildResult give you and what is TaskOutcome?

level: middleimportance: must knowfreq 45%

basics

~10 s

build() returns a BuildResult. You read result.output for console text and result.task(":path").outcome for a TaskOutcome enum like SUCCESS, UP_TO_DATE, FROM_CACHE, or SKIPPED, then assert on those.

open as a page

Inside a violationRules limit, what do the counter and value properties control, and how do they change what gets enforced?

level: middleimportance: must knowfreq 50%

basics

~10 s

counter picks the metric (INSTRUCTION, LINE, BRANCH, COMPLEXITY, METHOD, CLASS). value picks how it's measured (COVEREDRATIO, MISSEDCOUNT, TOTALCOUNT, etc.). Together with minimum/maximum they define the rule.

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 do you write a TestKit test that asserts a build fails as expected, and what does buildAndFail() return?

level: middleimportance: should knowfreq 35%

basics

~20 s

Use buildAndFail() instead of build() when you expect failure. It runs the build, expects a non-zero result, and returns a BuildResult whose output contains the error so you can assert on the failure message and the failing task's FAILED outcome.

open as a page

How does the 'element' scope in a JaCoCo violation rule work, and how would you enforce a per-class minimum while excluding generated code?

level: middleimportance: should knowfreq 38%

basics

~10 s

A rule's element sets the granularity it's applied at: BUNDLE (whole project, default), PACKAGE, CLASS, METHOD, or SOURCEFILE. Use element = CLASS to enforce per-class, and excludes to drop generated classes.

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

Generated and DTO classes are dragging down your JaCoCo numbers. How do you exclude them from the jacocoTestReport without touching coverage thresholds?

level: seniorimportance: should knowfreq 35%

basics

~10 s

Override the report's classDirectories to a filtered file tree, excluding paths like generated sources or config classes. Set classDirectories.setFrom(files(classDirectories.files.map { fileTree(it) { exclude("**/generated/**") } })).

open as a page

What is the jacoco { toolVersion } setting for, and why is jacocoTestReport often skipped as UP-TO-DATE?

level: seniorimportance: should knowfreq 40%

basics

~20 s

toolVersion pins the JaCoCo agent/ant version Gradle resolves, so it can handle your JDK's bytecode. jacocoTestReport is an incremental, cacheable task: if its inputs (exec data, classes) are unchanged, Gradle marks it UP-TO-DATE and skips it.

open as a page

What are the common pitfalls in TestKit functional tests around configuration-cache compatibility, test isolation, and output assertions, and how do you keep the suite reliable?

level: seniorimportance: should knowfreq 22%

basics

~10 s

Give each test a fresh @TempDir project, isolate the TestKit/Gradle home so runs don't share state, drive output deterministically, and add --configuration-cache to runs to catch config-cache violations early. Avoid asserting on default-log noise.

open as a page

How do you control the Gradle version and the execution mode (debug / in-process vs daemon) for a TestKit run, and why does it matter?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Use withGradleVersion("8.7") to test against a specific Gradle version, and withDebug(true) to run the build in the same JVM so you can set breakpoints. By default TestKit runs in a forked daemon at the runner's Gradle version.

open as a page

A team wants per-PR coverage enforcement that doesn't block on legacy code but holds the bar on new work. How do you structure jacocoTestCoverageVerification rules, and how do you wire and stage the gate in CI?

level: seniorimportance: should knowfreq 30%

basics

~10 s

Use multiple rules: a lenient BUNDLE floor everyone passes today, plus stricter per-CLASS/per-PACKAGE rules with includes/excludes targeting new or critical areas. Wire it into check, and ratchet thresholds up over time.

open as a page

Explain the task input/output relationship of jacocoTestCoverageVerification: what it depends on, what makes it up-to-date, and why running it alone can still execute tests.

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

The verification task consumes the test execution data (.exec) and the compiled classes as inputs. Because it needs that data, it's wired to run after test, so invoking it triggers test first. It's up-to-date when inputs and rules are unchanged.

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