1.2.1 Test Caching
Bazel caches test results the same way it caches build actions. If the test binary and all of its inputs are unchanged since the last run, Bazel skips re-execution entirely and replays the previous result1,2. This applies the incrementality introduced in 2.4 Caching & Incrementality to the pass/fail outcome of every test target as well as compilation.
What "PASSED (cached)" Means
Run a test twice without changing anything. The
test-output-debugging workspace also
supports this check with //:pass_test:
$ bazel test //:pass_test
//:pass_test PASSED in 0.1s
Executed 1 out of 1 test: 1 test passes.
$ bazel test //:pass_test
//:pass_test (cached) PASSED in 0.1s
Executed 0 out of 1 tests: 1 test passes.
The second run prints (cached) and reports Executed 0 out of 1 tests3. Bazel returned the stored result, including the test log and exit code, without launching the test binary1. The elapsed time shown is the original execution time rather than the replay time.
When multiple tests are involved, each is evaluated independently. A run might execute two fresh tests and replay three cached ones:
Executed 2 out of 5 tests: 5 tests pass.
How Test Caching Works
Test caching uses the same content-based hashing as build action caching. Bazel computes a key from the test binary, its runfiles, the command-line flags, and the test environment. If the key matches a previous passing run, the result is reused1,2.
The default behavior is controlled by --cache_test_results=auto, which re-runs a test only when4:
- Bazel detects changes in the test or its dependencies
- The test is tagged
external - Multiple runs were requested with
--runs_per_test - The test previously failed
Bazel caches only passing tests by default1. A failing test is always re-executed on the next invocation. This prevents a flake from getting stuck in cache: if a test fails due to a transient issue, the next run gets a fresh execution rather than a replayed failure1.
Only Passing Tests Are Cached
By caching only successes, Bazel ensures that a fresh run always has a chance to succeed. This policy also reduces flake exposure in CI: when a test passes on pre-submit and its inputs haven't changed by pre-release, the cached result is reused, leaving no opportunity for a flaky re-execution to produce a spurious failure1.
Controlling Test Caching
The --cache_test_results flag has three settings4:
| Value | Behavior |
|---|---|
auto (default) | Cache passing results. Re-run on failure, external tag, --runs_per_test, or input changes |
no | Re-run every test unconditionally |
yes | Like auto but also caches failures and --runs_per_test results |
Use --cache_test_results=no (or --nocache_test_results) when debugging a flaky test. It forces re-execution regardless of whether inputs changed5:
bazel test --nocache_test_results //lib:flaky_test
The yes setting is rarely useful in practice. It caches failures, which means a failing test stays "stuck" until its inputs change — the opposite of what most teams want.
Tests That Should Not Be Cached
Some tests depend on state outside the build graph: a running database, a network service, a system clock. Bazel cannot detect when that external state changes, so caching their results can produce false positives where the cached "pass" no longer reflects reality6.
Mark these tests with the external tag (covered alongside other behavioral tags in 1.2.3 Test Tags):
sh_test(
name = "integration_test",
srcs = ["run_integration.sh"],
tags = ["external"],
)
The external tag tells Bazel to skip caching for this target under --cache_test_results=auto4. The test runs fresh on every invocation.
Non-sandboxed tests (those using tags = ["local"] or relying on Docker) are another category where caching is risky. Results from non-sandboxed tests are best left uncached, because their outcomes are not guaranteed to be reproducible1. The connection to 2.3 Hermeticity & Sandboxing is direct: test caching is only correct when the test's result depends entirely on declared inputs.
The same reuse can save repeated work between CI stages when they share the relevant caches. 3.6.1 Basic CI Recipe places that mechanism in a complete CI flow. The prerequisite remains the same: identical declared inputs must produce the same outcome, without dependence on ambient host state.1,7
Bazel caches passing test results by default. If inputs haven't changed, Bazel reuses the result and prints (cached) PASSED. Failing tests are never cached under the default auto policy, so flakes don't get stuck. Use --nocache_test_results to force re-execution when debugging, and tag non-hermetic tests as external to opt them out of caching.
Check your understanding · 3 questions
1.What does '(cached) PASSED' mean in Bazel test output?
Select one answer
2.True or false: how does --cache_test_results=auto work?
Choose True or False for each sentence
3.Which of these conditions cause Bazel to re-run a test even if it previously passed (under --cache_test_results=auto)?
Select all that apply
Footnotes
-
BazelCon 2019 Day 1: Lightning Talk – Test Result Caching at Dropbox — passing-only caching policy, CI impact (10–20% fewer test runs), reproducibility as prerequisite, flake handling ↩1 ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
How Bazel Scales — tests as first-class build actions that accrue caching benefits ↩1 ↩2
-
Using Bazel to Improve Your Automated Test Suite — concrete before/after examples of
(cached) PASSEDoutput ↩ -
How to set up a Bazel testing configuration —
--cache_test_resultsflag values and the conditions for re-running tests ↩1 ↩2 ↩3 -
Debugging Flaky Tests at Google — using
--cache_test_results=noto bypass caching during flake debugging ↩ -
Bazel Training 101 (Part 14): How Bazel runs tests — false cache hits from untracked external state changes ↩
-
Ultimate Monorepo and Bazel for Building Apps at Scale — test determinism as prerequisite for effective test caching ↩