How do you assert on the result of a TestKit build — what does BuildResult give you and what is TaskOutcome?
answer
- BuildResult.output + task(":path").outcome
- TaskOutcome: SUCCESS/UP_TO_DATE/FROM_CACHE/SKIPPED/NO_SOURCE/FAILED
- task() null when not executed
- run twice -> assert UP_TO_DATE
- full :path required
basics
~10 sbuild() 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.
solid answer
~40 s`build()`/`buildAndFail()` return a **`BuildResult`**, the assertion surface for a functional test. Two main things to check: **`getOutput()`** — the full console output, useful for verifying logged/printed messages — and **per-task outcomes** via `result.task(":myTask")?.outcome`, which returns a `TaskOutcome`. The enum values are `SUCCESS`, `FAILED`, `UP_TO_DATE`, `SKIPPED`, `NO_SOURCE`, and `FROM_CACHE` — these let you verify not just that a task ran but *how* it resolved, which is essential for testing **incremental build** and **caching** behavior (e.g. run twice and assert the second run is `UP_TO_DATE`, or `FROM_CACHE` with the build cache enabled). `result.tasks(TaskOutcome.SUCCESS)` returns all tasks with that outcome, and `result.taskPaths(outcome)` their paths. Always use the full `:path` form, and null-safe access since `task()` returns null for tasks that didn't execute.
code
kotlin · 8 linesval result = runner.withArguments("build").build()
assertEquals(TaskOutcome.SUCCESS, result.task(":compileJava")?.outcome)
assertTrue(result.output.contains("BUILD SUCCESSFUL"))
// second run is incremental
val again = runner.build()
assertEquals(TaskOutcome.UP_TO_DATE, again.task(":compileJava")?.outcome)go deeper
Know that build() returns BuildResult and you read output and task outcome from it.
Enumerate TaskOutcome values and use them to test incremental/up-to-date behavior; handle the null-task case correctly.
Design tests that prove caching/incremental contracts (FROM_CACHE, UP_TO_DATE) and avoid brittle output assertions.
Set conventions for what plugin behaviors must be functionally asserted (outcomes vs output) and how those tests gate releases.
## BuildResult: the assertion surface Every terminal call (`build()` or `buildAndFail()`) returns a **`BuildResult`**. It is intentionally minimal — TestKit gives you black-box observability, not internal hooks. The members you use: - **`getOutput(): String`** — the entire console output of the build. Assert that expected log lines or `println` output appear (`result.output.contains("...")`). Note that output can be affected by the configured log level and the rich console; for stable assertions prefer explicit `logger.lifecycle`/`println` in the build under test. - **`task(taskPath: String): BuildTask?`** — looks up a single executed task by its **full path** (`":sub:myTask"`), returning `null` if that task did not execute. From a `BuildTask` you read `.outcome` and `.path`. - **`tasks(outcome: TaskOutcome): List<BuildTask>`** and **`tasks: List<BuildTask>`** — all executed tasks, optionally filtered by outcome. - **`taskPaths(outcome: TaskOutcome): List<String>`** — convenience for just the paths. ## TaskOutcome enum `TaskOutcome` captures *how* each task resolved: | Outcome | Meaning | |---|---| | `SUCCESS` | Task executed its actions and did work. | | `FAILED` | Task threw / failed. | | `UP_TO_DATE` | Inputs/outputs unchanged since last run; actions skipped. | | `FROM_CACHE` | Outputs restored from the build cache. | | `SKIPPED` | Explicitly skipped (e.g. `onlyIf` false). | | `NO_SOURCE` | Task had no source inputs to work on. | These distinctions let you write meaningful tests for **incremental behavior**: run the build once expecting `SUCCESS`, run it again with no changes and assert `UP_TO_DATE`; or enable `--build-cache`, clean, and assert `FROM_CACHE`. ## Example: verifying up-to-date behavior ```kotlin val runner = GradleRunner.create() .withProjectDir(projectDir) .withArguments("generate") .withPluginClasspath() val first = runner.build() assertEquals(TaskOutcome.SUCCESS, first.task(":generate")?.outcome) val second = runner.build() assertEquals(TaskOutcome.UP_TO_DATE, second.task(":generate")?.outcome) ``` ## Pitfalls - `task()` returns `null` for tasks that never ran (filtered out, or a dependency that didn't execute) — use null-safe access and don't confuse `null` with `SKIPPED`. - Always pass the **full `:path`**; `"generate"` without the leading colon will not match. - Asserting on `output` substrings is brittle if the build uses default logging — drive deterministic output from the build logic you control.
- How would you write a TestKit test that proves a task is cacheable?Run with `--build-cache`, capture SUCCESS on the first run, then clean outputs (or use a fresh project dir sharing the cache) and run again asserting `FROM_CACHE`. The cacheable task must be annotated `@CacheableTask` with properly declared inputs/outputs.
- Why might result.task(":foo") return null even though :foo exists in the build?Because :foo was not part of the executed task graph for that invocation — it wasn't requested or pulled in as a dependency, or it was excluded. null means 'did not execute', which is distinct from SKIPPED (executed-but-skipped).
saying these in an interview costs you the question
- Treating a null task() result as SKIPPED — they mean different things.
- Asserting on output substrings that depend on default log level rather than deterministic build output.
- Forgetting the leading colon in the task path.