How do you add an -update flag to a Go test so it rewrites its golden file in testdata?
answer
- an ordinary flag, not a testing feature
- package-level var, initialised before parsing
- write, then read back, then compare
- the whole-repo run trips over it
- under the flag the comparison cannot fail
basics
~20 sDeclare a package-level var in the test file with flag.Bool("update", false, ...). The testing package parses flags before tests run, so when it is set the test writes the fresh output with os.WriteFile, then reads it back and compares.
solid answer
~50 sYou register the flag the ordinary way — `var update = flag.Bool("update", false, "rewrite testdata golden files")` as a package-level variable in a `_test.go` file. Package variables are initialised before `testing.M.Run` parses the command line, so the flag is registered in time and `go test ./codegen -update` sets it. Inside the test, when `*update` is true you write the freshly generated bytes to the golden path with `os.WriteFile(path, got, 0o644)`; then, flag or not, you read the file back with `os.ReadFile` and compare with `bytes.Equal`. Two things to know: with `-update` the assertion is vacuous by construction — you wrote the file you are about to compare against — so the real review happens in the git diff afterwards. And `go test ./... -update` fails in every package whose test binary does not define that flag, with `flag provided but not defined: -update`.
code
go · 18 linesvar update = flag.Bool("update", false, "rewrite testdata golden files")
func TestGenerateUser(t *testing.T) {
got := Generate(userSchema)
golden := filepath.Join("testdata", "user.go.golden")
if *update {
if err := os.WriteFile(golden, got, 0o644); err != nil {
t.Fatal(err)
}
}
want, err := os.ReadFile(golden)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, want) {
t.Errorf("generated source differs from %s; rerun: go test ./codegen -update", golden)
}
}go deeper
Know the shape: a package-level flag.Bool in the test file, and go test with that flag rewrites the fixture instead of only reading it. Say that the flag is plain use of the flag package.
Explain the ordering — package variables initialise before the testing package parses the command line — and the write-then-read-then-compare body, including os.WriteFile and bytes.Equal.
Point out that under the flag the byte comparison cannot fail, so the assertion has moved to the git diff, and that a whole-repo run with the flag breaks every package that does not define it.
Decide the team's convention: which packages own goldens, whether regeneration is a flag or an environment variable, and what the rule is for reviewing regenerated fixtures before they land.
## The problem the flag solves A golden test asserts that a function's output equals the bytes of a checked-in file. When the output is supposed to change — you taught a code generator to emit a new struct tag, say — somebody has to produce the new file. Hand-editing a 400-line generated file is error-prone and pointless, so the test itself grows a switch: run it in *record* mode and it rewrites the fixture from its own output. ## Registering the flag There is nothing test-specific about it; you use `flag` exactly as any program would, at package scope in a `_test.go` file: ```go var update = flag.Bool("update", false, "rewrite testdata golden files") ``` Why package scope matters: `flag.Bool` registers the flag on the command-line `FlagSet` as a side effect of the variable's initialisation, and package variables are initialised before any function runs. The `testing` package parses the command line inside `M.Run`, before it starts any test. Register the flag inside a test function instead and you are too late — parsing has already happened and the process will have failed on the unknown flag. ## Using it ```go func TestGenerateUser(t *testing.T) { got := Generate(userSchema) golden := filepath.Join("testdata", "user.go.golden") if *update { if err := os.WriteFile(golden, got, 0o644); err != nil { t.Fatal(err) } } want, err := os.ReadFile(golden) if err != nil { t.Fatal(err) } if !bytes.Equal(got, want) { t.Errorf("generated source differs from %s; rerun with -update", golden) } } ``` Writing first and then reading back — rather than skipping the comparison when updating — costs nothing and keeps a single code path. `0o644` is the conventional permission; the file is source-controlled content, not a secret. ## Running it `go test ./codegen -update`. The `go` command passes flags it does not recognise through to the test binary, which is why a custom flag needs no special registration with the tool. The sharp edge appears the first time somebody types `go test ./... -update` in a repo with more than one package. Each package is built into its **own** test binary, and only the binaries whose package declares `-update` know the flag. Every other one exits with `flag provided but not defined: -update` and the run goes red. The fix is to scope the command to the packages that own goldens, and to say so in the test's failure message: `rerun: go test ./codegen -update`. Combine it with `-run` when only one case needs regenerating: `go test ./codegen -run TestGenerateUser/order_with_tags -update`. ## Deriving the path from the test name Table-driven suites often build the fixture path from `t.Name()` so each case gets its own file. Remember that a subtest's name contains the parent name and a `/`, and that spaces in a case name become underscores. Used raw as a path, `t.Name()` will create nested directories under `testdata` — sanitize it, for example with `strings.ReplaceAll(name, "/", "_")`, or build the path from the case's own name field instead. ## The property that makes people uneasy, correctly Under `-update` the test **cannot fail** the byte comparison: it wrote the file it is comparing against. That is not a bug in the pattern, but it does relocate the assertion. The thing that actually decides whether the new output is correct is a human reading `git diff` on the regenerated file — which is why you regenerate on a clean working tree, and why gigantic single-file goldens are a review hazard. It also means a regenerated golden must be followed by a plain `go test` run (no flag) before you trust the suite again. ## Alternatives you may be asked about An environment variable (`UPDATE_GOLDEN=1` read with `os.Getenv`) does the same job and survives `go test ./...` because an unknown env var breaks nothing. The trade is discoverability: `go test -h` lists a registered flag with its usage string, and an env var is invisible unless documented. Most Go codebases still use the flag, and `-update` is the near-universal spelling.
- Why does registering the flag inside the test function instead of at package scope fail?`flag.Bool` only registers the flag when it runs, and the `testing` package parses the command line inside `M.Run` before any test function executes. A registration inside the test happens after parsing, so the binary has already rejected `-update` as an unknown flag. Package-level variable initialisation runs first, which is why the declaration belongs at package scope in a `_test.go` file.
- What happens when you run go test ./... -update across a repo where one package defines the flag?Every package is compiled into its own test binary, so only that one package's binary knows `-update`. The rest exit immediately with `flag provided but not defined: -update` and the whole run is red. Scope the command to the packages that own golden files, and put the exact command in the test's failure message so nobody guesses.
- How would you name the golden file for each case in a table-driven golden test?Derive it from the case's own name field, or from `t.Name()` after sanitizing it — a subtest name contains a `/` separator and turns spaces into underscores, so using it raw as a path creates nested directories under `testdata`. One file per case keeps the regenerated diff readable, which is where the real review of a golden happens.
saying these in an interview costs you the question
- Registers the flag inside the test function
- Believes go test needs to be taught about custom flags
- Thinks a green run under -update proves anything
- Runs go test ./... -update and blames the tooling
- Skips reading the file back after writing it