skip to content

`go test ./...` keeps reporting (cached) for a package whose behaviour changed — how do you diagnose it?

level: seniorimportance: should knowfreq 36%

answer

  1. the marker means nothing executed
  2. compare the same package with -count=1
  3. list what the run could not have recorded
  4. clear the history before claiming a conclusion
  5. either make the input visible or never cache that suite

basics

~20 s

Re-run that package with go test -count=1 and compare. If the outcome changes, the test depends on something the go command never recorded: a service, a database, the clock, or a file outside the package's source root.

solid answer

~50 s

Start by confirming it really is reuse — the `(cached)` marker means the binary did not run — then re-run just that package with `-count=1`. If it now fails, or takes noticeably longer, the result was keyed on inputs that no longer describe reality. Next, ask what the test actually consumes. The go command records the test binary's identity, the cacheable flags, and the files under the package's source root and environment variables the binary read. Anything else is invisible: rows in a database, a response from a service, the wall clock, a file read by absolute path from elsewhere in the monorepo, or a helper binary the test shells out to. Clear the poisoned history with `go clean -testcache` to get a clean baseline. The fix is structural: move fixtures into the package's own `testdata` and read configuration from the environment so the go command can see them, and give suites that genuinely touch outside state a permanent `-count=1`.

go deeper

for a junior

Recall the first move: run that one package again with -count=1 and see whether the answer changes before theorising about anything else.

for a middle

Explain why the stale pass is possible at all — the recorded inputs are the binary, the cacheable flags and the package-local file and environment reads, and nothing else.

for a senior

Show a real diagnostic order: confirm reuse, isolate with -count=1, enumerate untracked dependencies, clear the history to get a clean baseline, then fix the test's inputs rather than typing flags forever.

for a principal

Own the split between hermetic packages that stay cacheable and suites that are declared uncacheable in one place, and be able to defend not throwing away reuse across the whole repo to cover a few offenders.

## The symptom A repo-wide `go test ./...`, typically driven from a `make test` target across a multi-module monorepo, reports a package as `ok ... (cached)` while a developer insists the behaviour under test has changed. The developer stops trusting green runs, which is a far more expensive outcome than the bug itself. ## Step 1: establish that reuse is really what you are seeing `(cached)` in the summary line means the test binary was not executed and stored output was replayed. That is worth stating out loud, because the alternative explanations — the wrong package being run, a `make` target that filters packages, a module in the monorepo that the `./...` pattern never reached — look identical from a distance and are at least as common. In a multi-module repo, `./...` covers only the current module's packages, so a package that appears not to react may simply not be in the run at all. ## Step 2: re-run the one package with `-count=1` ``` go test -count=1 ./internal/rates/ ``` This is the cheap discriminator. If the package now fails, you have your answer: the stored result was matched on inputs that do not capture what the test really depends on. If it still passes, the cache was telling the truth and your suspicion belongs somewhere else — the change you made may genuinely not be exercised by that package's tests, which is a coverage problem, not a caching problem. ## Step 3: work out which input was invisible The go command's evidence is limited to three things: the test binary's identity (package source plus all dependencies plus build settings), the flags on the command line, and the reads the binary performed — files under the package's source root and environment variables it consulted. Everything else it cannot observe. The usual culprits, in rough order of frequency: - **A live dependency.** The test queries a database, calls an HTTP service, or reads a queue. The data changed; the binary and its recorded reads did not. - **A file outside the package's source root.** A fixture shared across the monorepo and read by an absolute or `../../` path, or a config file in a system directory. The recording covers the package's own tree. - **A generated artefact.** A `make` step generates code or data, and the test consumes the *output* rather than the generator. If the generated Go source is regenerated the binary changes and the cache reacts correctly; if the test reads a generated non-Go file from outside its tree, it does not. - **A helper binary or subprocess.** The test executes a compiled tool. The go command has no idea that tool was rebuilt. - **The clock or other ambient state.** A test whose outcome depends on the date passes forever from a recording. A quick way to shortlist: read the test for `os.Getenv` and file reads relative to the package (those are tracked and therefore innocent), and treat everything else it touches as suspect. ## Step 4: reset the baseline Once you have a hypothesis, `go clean -testcache` expires every stored result so nothing from the poisoned history can contribute. Run the suite once and watch it execute, change the suspect input, run again, and see whether the second run reports `(cached)`. If it does, you have demonstrated the invisibility directly, which is a far better bug report than "the cache is broken". ## Step 5: fix it structurally, not with a habit The wrong fix is a `-count=1` reflex typed by whoever remembers, or a `go clean -testcache` wired into a script — the first is unreliable, and the second discards everyone's results while still allowing the next run to cache. Two durable fixes, in preference order: 1. **Make the input visible.** Move the fixture into the package's own `testdata` directory so reading it is recorded; take configuration from environment variables that the test reads, so a changed value invalidates the result for free. A hermetic package test then invalidates itself correctly and you keep the speed. 2. **Declare the suite uncacheable.** For tests that genuinely depend on outside state, put `-count=1` on the command that runs them, or set `GOFLAGS=-count=1` for that job. Say it in one place, in the `Makefile` target that runs the integration suite, so nobody has to remember. The judgment being tested here is that you separate the two populations rather than punishing the whole repo. Blanket-disabling reuse across a large monorepo throws away real minutes on every developer machine to compensate for a handful of non-hermetic packages, and it leaves those packages just as untrustworthy the moment somebody drops the flag.

  • The package passes with -count=1 too. What is your next hypothesis?
    That caching was never involved in the complaint. Likely candidates: the change is not exercised by that package's tests at all, the make target filters the package set, or in a multi-module monorepo the package sits in a module that ./... from the current module never reaches.
  • A test reads a shared fixture from ../../testdata via a relative path. Is that read tracked?
    Treat it as not tracked. The recording covers files under the package's own source root, so a fixture reached outside it can change without invalidating anything. Copy or generate it into the package's testdata directory, or accept the suite as uncacheable.
  • Why not simply set GOFLAGS=-count=1 for the entire monorepo?
    It works, but it pays for a handful of non-hermetic packages with every developer's repeated runs across hundreds of hermetic ones. Better to keep unit packages cacheable and honest, and scope the bypass to the jobs that genuinely touch outside state.
  • How do you prove the invisibility to a sceptical reviewer?
    Run go clean -testcache, run the package once and watch it execute, change the suspect input without touching any Go source, run again and show the cached marker. That is a reproducible demonstration that the input is outside what the go command records.

The go command signs off on a run using only the paperwork it collected. Anything the test reached for without leaving a record is free to change, and the signature stays valid.

saying these in an interview costs you the question

  • Blames the go command for a broken cache instead of an unrecorded input
  • Wires go clean -testcache into every test invocation
  • Assumes a database or HTTP response participates in the cache key
  • Never checks whether the package was in the run at all
  • Disables result reuse repo-wide to fix two non-hermetic packages
  • Concludes from a single -count=1 pass without clearing stored history