skip to content

How do you assert on task results from a BuildResult, and what do the different TaskOutcome values mean?

level: middleimportance: must knowfreq 50%

answer

  1. task(":x").outcome → TaskOutcome
  2. SUCCESS / UP_TO_DATE / FROM_CACHE / SKIPPED / NO_SOURCE / FAILED
  3. tasks(outcome), taskPaths(outcome)
  4. run twice → assert UP_TO_DATE
  5. result.output for messages

basics

~10 s

BuildResult.task(":path").outcome returns a TaskOutcome enum: SUCCESS, UP_TO_DATE, FROM_CACHE, SKIPPED, NO_SOURCE, FAILED. You assert the expected outcome plus check result.output for messages.

solid answer

~40 s

After running the build, `BuildResult` exposes the per-task results. Use `result.task(":myTask")?.outcome` for one task, or `result.tasks(TaskOutcome.SUCCESS)` / `result.taskPaths(TaskOutcome.UP_TO_DATE)` to query by outcome. The `TaskOutcome` enum captures *why* a task did or didn't do work: **SUCCESS** (executed and did work), **FAILED** (threw), **UP_TO_DATE** (skipped because inputs/outputs unchanged), **FROM_CACHE** (outputs restored from the build cache), **SKIPPED** (excluded via onlyIf or `-x`), and **NO_SOURCE** (declared inputs were empty). These let you write precise assertions — e.g., run a build twice and assert the second run is `UP_TO_DATE` to prove incremental support, or assert `FROM_CACHE` with `--build-cache` to prove cacheability. You also commonly assert on `result.output` for console messages.

code

kotlin · 15 lines
kotlin
val result = GradleRunner.create()
    .withProjectDir(dir)
    .withArguments("--build-cache", "generate")
    .withPluginClasspath()
    .build()

assertEquals(TaskOutcome.SUCCESS, result.task(":generate")!!.outcome)

// second run, outputs deleted -> should come from cache
val cached = GradleRunner.create()
    .withProjectDir(dir)
    .withArguments("--build-cache", "generate")
    .withPluginClasspath()
    .build()
assertEquals(TaskOutcome.FROM_CACHE, cached.task(":generate")!!.outcome)

go deeper

for a junior

Know task(...).outcome returns SUCCESS/FAILED and you can check output.

for a middle

Enumerate the outcomes and use the run-twice pattern to test up-to-date behavior.

for a senior

Design tests that prove cacheability and conditional execution; distinguish all six outcomes precisely.

for a principal

Set assertion conventions so plugin correctness (incremental/cacheable) is regression-protected across the codebase.

## Reading results off BuildResult `BuildResult` is returned by `build()` and `buildAndFail()`. Key accessors: - `task(":a:b")` → a `BuildTask?` (its `.outcome` and `.path`). - `tasks(TaskOutcome.SUCCESS)` → all tasks with that outcome. - `taskPaths(TaskOutcome.UP_TO_DATE)` → just the paths. - `output` → the full console text (assert messages here). ## The TaskOutcome enum | Outcome | Meaning | |---|---| | `SUCCESS` | Task executed and performed work. | | `FAILED` | Task threw / build failed at this task. | | `UP_TO_DATE` | Skipped: declared inputs & outputs unchanged since last run. | | `FROM_CACHE` | Outputs restored from the build cache instead of executing. | | `SKIPPED` | Explicitly skipped (`onlyIf {false}` or `-x`). | | `NO_SOURCE` | Task ran but had no source/inputs to act on. | ## Why each matters in tests These outcomes are how you prove non-functional plugin qualities: - **Incrementality / up-to-date checking:** run the same build twice; first `SUCCESS`, second `UP_TO_DATE`. - **Cacheability:** run with `--build-cache`, clear outputs, run again, assert `FROM_CACHE` (requires a `@CacheableTask`). - **Conditional execution:** assert `SKIPPED` when `onlyIf` is false. ## Example: proving up-to-date behavior ```kotlin val first = runner.withArguments("generate").build() assertEquals(TaskOutcome.SUCCESS, first.task(":generate")!!.outcome) val second = runner.withArguments("generate").build() assertEquals(TaskOutcome.UP_TO_DATE, second.task(":generate")!!.outcome) ``` ## Assert on output too Outcome answers *did it run*; `output` answers *what did it say/produce*. Pair them — e.g. assert `SUCCESS` and that the log contains your expected message — for robust tests.

  • How would you prove a task is properly incremental in a TestKit test?
    Run the build, assert SUCCESS, run the identical build again without changing inputs, and assert the task is UP_TO_DATE.
  • What is the difference between UP_TO_DATE and FROM_CACHE?
    UP_TO_DATE means outputs already present locally and inputs unchanged, so nothing ran. FROM_CACHE means outputs were missing locally but restored from the build cache because the task is cacheable and the cache key matched.
  • When does NO_SOURCE appear?
    When a task's declared source/input collection is empty, so there is nothing to process — distinct from being up-to-date.

saying these in an interview costs you the question

  • Treating UP_TO_DATE and SKIPPED as the same thing.
  • Asserting only on output and never on TaskOutcome (misses incremental/caching regressions).
  • Expecting FROM_CACHE without enabling --build-cache or marking the task @CacheableTask.

context