skip to content

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%

answer

  1. finalizedBy = runs even on test failure
  2. dependsOn = correct input ordering
  3. use both directions
  4. test -> finalizedBy report
  5. report -> dependsOn test

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.

solid answer

~50 s

There are two complementary wirings, and the right answer uses both directions for different reasons. First, `jacocoTestReport` should **depend on** `test` (`dependsOn(tasks.test)`) because the report's input is the execution data the test run produces — if someone invokes the report directly, tests must run first. Second, `test` should **finalize** with the report (`finalizedBy(tasks.jacocoTestReport)`) so that running `./gradlew test` automatically generates coverage afterward, even if tests fail (a finalizer still runs, so you get partial coverage of what executed). `finalizedBy` is preferred over `dependsOn` for the test→report direction because it guarantees ordering without making the report a prerequisite of test, and it runs after failures. You can also fold the report into the gate by making `check` depend on it. Avoid only using `dependsOn` from report to test if you also want `./gradlew test` alone to emit reports.

code

kotlin · 11 lines
kotlin
tasks.test {
    finalizedBy(tasks.jacocoTestReport)
}

tasks.jacocoTestReport {
    dependsOn(tasks.test)
    reports {
        xml.required.set(true)
        html.required.set(true)
    }
}

go deeper

for a junior

Know you must run jacocoTestReport (or wire it) because test alone won't generate the report.

for a middle

Distinguish finalizedBy vs dependsOn and why both directions are useful.

for a senior

Explain failure-tolerance of finalizers and folding the report into check; keep references lazy.

for a principal

Bake the wiring into a shared convention plugin so coverage is produced uniformly across all modules and CI never forgets a step.

## The two relationships There are two distinct task relationships at play, and confusing them is a classic interview trap. ### 1. `dependsOn` — correctness of inputs `jacocoTestReport` reads `build/jacoco/test.exec`. That file only exists after `test` runs. So if a user runs `./gradlew jacocoTestReport` directly, tests must execute first: ```kotlin tasks.jacocoTestReport { dependsOn(tasks.test) } ``` Without this, invoking the report alone could produce an empty/stale report. ### 2. `finalizedBy` — convenience and failure-tolerance You usually want `./gradlew test` to *also* emit coverage. Making the report a finalizer of `test` does that: ```kotlin tasks.test { finalizedBy(tasks.jacocoTestReport) } ``` `finalizedBy` has a key property: **a finalizer runs even if the finalized task fails.** So when some tests fail, you still get a report covering whatever executed — invaluable for debugging. If you instead used `dependsOn` in this direction, a test failure would abort before the report ran. ## Why not just one? - Only `finalizedBy(test → report)`: running the report *directly* wouldn't guarantee tests ran first (no input dependency). - Only `dependsOn(report → test)`: running `./gradlew test` alone wouldn't produce a report, and the report wouldn't run after a failure. Using both covers every invocation path. ## Folding into the gate Many teams also want coverage as part of `check`: ```kotlin tasks.check { dependsOn(tasks.jacocoTestReport) } ``` ## Putting it together ```kotlin tasks.test { finalizedBy(tasks.jacocoTestReport) } tasks.jacocoTestReport { dependsOn(tasks.test) reports { xml.required.set(true); html.required.set(true) } } ``` ## Mechanics note These wirings use lazy task references (`tasks.test`, `tasks.jacocoTestReport`) so the tasks aren't eagerly realized — keeping configuration fast and configuration-cache friendly.

  • Why use `finalizedBy` for the test→report link instead of `dependsOn`?
    A finalizer runs even when the finalized task fails, so you still get a coverage report for whatever tests executed before the failure. `dependsOn` in that direction would abort the report on a test failure.
  • If you only add `jacocoTestReport.dependsOn(test)`, does `./gradlew test` produce a report?
    No. That only ensures tests run before the report when the report is invoked; running `test` by itself won't trigger the report. You need `test.finalizedBy(jacocoTestReport)` for that.
  • How do you make coverage part of the standard `check` gate?
    `tasks.check { dependsOn(tasks.jacocoTestReport) }`, so `./gradlew check` generates the report alongside other verification.

saying these in an interview costs you the question

  • Saying `dependsOn` alone makes `./gradlew test` emit a report — it doesn't.
  • Claiming a finalizer is skipped when the finalized task fails — it actually still runs.

context