skip to content

How would you make Gradle fail when a test task runs zero tests, and surface noisy stdout only when debugging?

level: seniorimportance: nice to knowfreq 22%

answer

  1. zero tests still passes by default
  2. afterSuite root descriptor parent == null
  3. result.testCount == 0 -> throw
  4. testLogging per log level
  5. showStandardStreams under info {}

basics

~20 s

By default a Test task that matches zero tests still passes, which hides broken filters. You can use a beforeSuite/afterSuite listener counting executed tests, or TestFrameworkOptions failure-on-no-tests, and gate stdout behind the info log level's testLogging so it's quiet normally.

solid answer

~40 s

Two distinct concerns. (1) **Zero-test detection:** an empty selection (a typo'd `--tests` filter or misconfigured includes) usually exits green, masking a real problem. You can add an `afterSuite` listener on the root suite that inspects `TestDescriptor.parent == null` and fails if the result's `testCount == 0`. (Newer JUnit Platform launcher options also support failing on no discovered tests.) (2) **Noisy streams:** capturing `STANDARD_OUT`/`STANDARD_ERROR` is invaluable when debugging but floods normal runs, especially with parallel forks. The clean approach is to configure `testLogging.info { showStandardStreams = true }` so streams only appear when you pass `--info`, keeping the default lifecycle output clean. This leverages the fact that `testLogging` is configured **per log level**.

code

kotlin · 10 lines
kotlin
tasks.withType<Test>().configureEach {
    testLogging {
        info { showStandardStreams = true } // noisy streams only on --info
    }
    afterSuite(KotlinClosure2<org.gradle.api.tasks.testing.TestDescriptor, org.gradle.api.tasks.testing.TestResult, Unit>({ desc, result ->
        if (desc.parent == null && result.testCount == 0L) {
            throw GradleException("No tests executed for '$name'")
        }
    }))
}

go deeper

for a junior

Recognize that an empty test selection can still pass green; the rest is advanced.

for a middle

Know that standard streams can be captured via testLogging and that it gets noisy, plus the basic zero-test risk.

for a senior

Implement the afterSuite zero-test guard and the per-level showStandardStreams gating, explaining why each works.

for a principal

Bake both into a shared convention plugin so every module is red-when-empty and quiet-but-debuggable by default; weigh log-volume and CI-cost trade-offs.

## Concern 1 — a green build that ran nothing Gradle does not, by default, fail a `Test` task that selected zero tests. So a broken include pattern, a renamed package, or a typo in `--tests "com.acme.Fooo"` produces a passing build that tested nothing — a dangerous false green. ### Detecting it with a suite listener `AbstractTestTask` lets you register listeners. The **root** suite descriptor has a null parent; its result carries `testCount`: ```kotlin import org.gradle.api.tasks.testing.TestDescriptor import org.gradle.api.tasks.testing.TestResult tasks.withType<Test>().configureEach { afterSuite(KotlinClosure2<TestDescriptor, TestResult, Unit>({ desc, result -> if (desc.parent == null && result.testCount == 0L) { throw GradleException("No tests were executed for task '$name'") } })) } ``` (With the JUnit Platform launcher you can alternatively configure a `failIfNoTests` style option; the listener approach is engine-agnostic.) ## Concern 2 — stdout/stderr noise Capturing standard streams (`showStandardStreams = true`, or adding `STANDARD_OUT`/`STANDARD_ERROR` to `events`) is great when chasing a flaky failure but overwhelming on a normal run — and even more so with `maxParallelForks` interleaving output from several JVMs. ### Gate it behind the info level `testLogging` is configured **per log level**. Configure streams only at `info`, so they appear when you run `--info` but stay silent otherwise: ```kotlin tasks.withType<Test>().configureEach { testLogging { // quiet default lifecycle output exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL info { showStandardStreams = true // only with --info } } } ``` ## Why this matters Both patterns are about trustworthy signal: a build should be **red when nothing ran** and **quiet but debuggable** on demand. They sit alongside `ignoreFailures`/`failFast`/`events`/`exceptionFormat` as part of a deliberate failure-and-logging policy for the test task.

  • Why does configuring `showStandardStreams` inside an `info { }` block keep normal runs quiet?
    Because `testLogging` settings are scoped per log level. Settings under `info` only take effect when Gradle runs at the info level (e.g. `--info`); the default lifecycle level is unaffected, so streams stay hidden normally.
  • How do you identify the root test suite in an afterSuite listener?
    The root suite's `TestDescriptor.parent` is null. Checking `desc.parent == null` ensures you read the aggregate result once, where `result.testCount` reflects the whole run.

saying these in an interview costs you the question

  • Assuming Gradle fails by default when zero tests match — it doesn't.
  • Enabling standard-stream capture globally and drowning normal CI logs, especially under parallel forks.
  • Confusing per-level testLogging blocks with the default lifecycle config.

context