skip to content

Which inputs decide whether `go test` may reuse a cached result for a package?

level: middleimportance: should knowfreq 44%

answer

  1. the binary, the flags, and what the run observed
  2. dependency edits travel through the binary
  3. only a small set of flags is cacheable
  4. the test binary logs its file and environment reads
  5. testdata is ignored for building, not for caching

basics

~20 s

Three inputs: the test binary, built from the package and its dependencies; the command line, which must use only cacheable flags; and the files under the package's source tree and the environment variables the test read.

solid answer

~40 s

The go command matches on the test binary first — that binary is built from the package's own source plus every dependency, plus the toolchain and build settings, so any code change on that graph produces a different entry. Second, every flag on the command line must come from the cacheable set (`-run`, `-v`, `-short`, `-timeout`, `-cpu`, `-parallel`, `-failfast`, `-list`, `-fullpath`); anything else, `-count` included, makes the invocation uncacheable outright. Third, and this is the part people miss, the test binary records what it consulted at runtime: files it opened under the package's source root and environment variables it read. A stored entry only matches a future run in which those file contents and those variable values are unchanged, which is why editing a `testdata` fixture correctly invalidates the result.

code

go · 10 lines
go
func TestRateTable(t *testing.T) {
	raw, err := os.ReadFile("testdata/rates.json")
	if err != nil {
		t.Fatal(err)
	}
	region := os.Getenv("BILLING_REGION")
	if len(raw) == 0 || region == "" {
		t.Fatal("empty fixture or empty region")
	}
}

go deeper

for a junior

Recall that a code change in the package or in something it imports is enough to make the tests run again, and that a fixture file the test reads counts too.

for a middle

Be able to name all three layers unprompted — binary identity, the restricted flag set, and the recorded file and environment reads — and explain why -count=1 defeats reuse.

for a senior

Use the boundary deliberately: shape fixtures and configuration so the go command can observe them, and mark suites that read outside state as never-cacheable rather than hoping.

for a principal

Own the convention that decides which suites are allowed to be reusable at all, and the cost tradeoff between fast repeated repo-wide runs and a green that always reflects reality.

## Three layers, checked in order A cached test result is only reused when the go command can convince itself that the run would be identical. It does that with three layers of evidence. ### 1. The test binary The primary key is the identity of the test binary the run would use. That binary is derived from the package's own `.go` files, the `_test.go` files, every package it imports transitively, the compiler and toolchain version, and the build configuration (`GOOS`, `GOARCH`, build flags). Change any of that and you get a different binary identity and therefore a different cache entry — the old result is not invalidated so much as simply not found. This is why editing an unrelated package leaves a test showing `(cached)`, and why editing a package that the test imports does not: dependency edits move through the binary, unrelated edits do not. ### 2. The command line The go command will only cache a run whose flags all come from a restricted set it knows is safe to key on: `-run`, `-v`, `-short`, `-timeout`, `-cpu`, `-parallel`, `-failfast`, `-list`, `-fullpath`. Two consequences follow. - Different values of those flags are *different entries*, not invalidations. `go test -run TestA` and `go test -run TestB` each get their own stored result, and each can be replayed. - Any flag or argument outside the set makes the run uncacheable: it is not matched against the cache, and its result is not stored. That is the whole mechanism behind `-count=1`. `-count=1` runs the tests exactly once, which is already the default, so its only effect is to take the invocation out of the cacheable set. Custom flags your own `_test.go` file defines have the same effect, which sometimes surprises people who wonder why one particular suite "never caches". One related quirk worth knowing: a cached result is treated as having taken no time at all, so a stored pass is reused regardless of the `-timeout` value you pass. ### 3. What the test observed while running The layer that makes the feature usable in practice. While the test binary runs, the testing infrastructure records the files it opens and stats under the package's source root, and the environment variables it reads. Those observations are stored alongside the result. A future run matches only if those files still hash the same and those variables still hold the same values. So a test that does this: ```go raw, err := os.ReadFile("testdata/rates.json") region := os.Getenv("BILLING_REGION") ``` is correctly invalidated when you edit `testdata/rates.json`, and correctly invalidated when you re-run with a different `BILLING_REGION`. This is a common source of surprise in both directions. Some developers assume `testdata` is invisible because the go command ignores `testdata` directories when *building* packages — true for building, false for result caching. Others assume the environment is not tracked and are startled when flipping a variable re-runs everything. ### The part that is not tracked The recording covers files under the package's source root. Reads that reach outside it, and everything that is not a file read or an environment lookup at all, are invisible: rows in a database, a response from an HTTP service, the wall clock, a message on a queue, the output of a helper binary the test shells out to. The cache cannot notice those changing, which is precisely how a stale green is produced. ## Designing for it Two practical habits fall out. First, keep fixtures inside the package as `testdata` files and read configuration through the environment, so the inputs a test really depends on are ones the go command can see — the test then invalidates itself for free. Second, for suites that genuinely touch outside state, stop pretending: run them with `-count=1` so no result is ever stored or reused. Wanting reuse for something the go command cannot observe is the wish that produces untrustworthy greens. ## Where the entries live All of this lives in the build cache directory reported by `go env GOCACHE`, alongside compiled package artefacts. `go clean -testcache` expires the test results there without touching the compiled artefacts; `go clean -cache` removes everything and makes your next build slow.

  • Do `go test -run TestA ./pkg` and `go test -run TestB ./pkg` share a cache entry?
    No. Cacheable flags are part of the key, so each distinct flag value gets its own stored result. Both can be replayed independently, which is why alternating between two -run values can produce two instant runs in a row.
  • A test defines its own flag with the flag package. Why does that package never show (cached)?
    Because passing a flag outside the cacheable set makes the invocation uncacheable — nothing is matched and nothing is stored. If the flag is always supplied, that package simply never participates in result reuse.
  • Does raising -timeout invalidate a stored pass?
    No. -timeout is a cacheable flag and a stored successful result is treated as having taken no time at all, so it is reused regardless of the timeout you ask for. The timeout only ever constrains a run that actually executes.
  • Why does editing a testdata fixture invalidate the result when the go command ignores testdata directories?
    Two different rules. testdata is excluded when the go command decides which directories contain buildable packages. Result caching works from what the test binary actually opened at runtime, so a fixture read under the package's source root is recorded and hashed like any other input.

The cache entry is a receipt listing what the run consumed. Change anything on the receipt and the go command reruns; anything the run consumed without putting it on the receipt can change silently.

saying these in an interview costs you the question

  • Thinks only .go files affect the cache key
  • Says testdata is ignored so fixtures cannot invalidate a result
  • Believes every flag value simply invalidates the cache rather than keying it
  • Assumes environment variables the test reads are untracked
  • Claims a database or HTTP response is part of the recorded inputs