Day 35 — Golden files & Cleanup
Day 35 — Golden files & Cleanup
Stage IV · ~3h (theory-heavy)
Goal: Use golden-file (snapshot) tests for encodings and rendered output; manage temp files with t.TempDir and t.Cleanup; know when goldens help vs harm.
Golden tests freeze observable output. They excel at codecs, CLIs, and formatters—and rot when output is noisy or non-deterministic.
Why this day exists
Some correctness is “matches the fixture”:
- Pretty-printed JSON
- Serialized config
- CLI help/output slices
- Canonical error messages for stable APIs
Hand-asserting large strings is painful. Goldens store expected bytes on disk.
Theory 1 — Golden file pattern
testdata/
foo.input.json
foo.golden.json
func TestEncode(t *testing.T) {
in, err := os.ReadFile("testdata/foo.input.json")
if err != nil {
t.Fatal(err)
}
var v Value
if err := json.Unmarshal(in, &v); err != nil {
t.Fatal(err)
}
got, err := EncodePretty(v)
if err != nil {
t.Fatal(err)
}
wantPath := "testdata/foo.golden.json"
if *update {
if err := os.WriteFile(wantPath, got, 0o644); err != nil {
t.Fatal(err)
}
}
want, err := os.ReadFile(wantPath)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, want) {
t.Fatalf("mismatch\n got: %s\nwant: %s", got, want)
}
}Update flag
var update = flag.Bool("update", false, "update golden files")go test -update ./...
# note: flag must be defined in TestMain or via flag.Parse in init carefullyCommon pattern:
func TestMain(m *testing.M) {
flag.Parse()
os.Exit(m.Run())
}Or use environment variable UPDATE_GOLDEN=1 to avoid flag plumbing conflicts.
Theory 2 — t.TempDir and t.Cleanup
dir := t.TempDir() // auto-removed after test
path := filepath.Join(dir, "out.txt")f, err := os.CreateTemp("", "day35-*")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { os.Remove(f.Name()) })Rules:
- Prefer
t.TempDirover manual deletes
t.Cleanupruns even on failure (LIFO)
- Don’t rely on process exit for temp hygiene in long test binaries
Go 1.26 also adds t.ArtifactDir() for intentional retained outputs when -artifacts is set—use for CI-collected blobs; goldens stay in testdata/.
Theory 3 — Determinism requirements
Goldens fail if output includes:
- Timestamps
- Map iteration order (for text that ranges maps)
- Random IDs
- Absolute paths
Fixes:
- Inject clock
- Sort keys (
json.Encoder.SetIndent+ structured maps with sorted encode)
- Normalize paths in comparison
- Compare AST/semantic equality instead of bytes when needed
Theory 4 — Diff UX
On mismatch, print a readable diff:
import "github.com/google/go-cmp/cmp" // optional dep
// or simple line-oriented diff helperFor labs, a short helper is enough:
func diff(a, b string) string {
// naive: show first mismatch index
}Never dump megabytes without truncation in t.Fatalf.
Theory 5 — When not to golden
| Prefer unit asserts | Prefer golden |
|---|---|
| Numeric results | Large structured text |
| Error sentinel types | Exact formatted documents |
| Branch logic | Regression on pretty printers |
Goldens that update weekly become ignored noise—treat updates as reviews.
Theory 6 — Encoding lab target
Pick one:
- Canonical JSON for a config struct
- Custom line protocol encode/decode
- Markdown table renderer
Round-trip tests + golden for encode path.
Worked example — stable JSON golden
package conf
import (
"bytes"
"encoding/json"
)
type Config struct {
Name string `json:"name"`
Ports []int `json:"ports"`
Tags []string `json:"tags"`
}
func Encode(c Config) ([]byte, error) {
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetIndent("", " ")
if err := enc.Encode(c); err != nil {
return nil, err
}
return buf.Bytes(), nil
}Test uses testdata/config.golden.json. Note Encode adds trailing newline—golden must match.
Worked example — Cleanup with servers
func TestServer(t *testing.T) {
// if using httptest:
// srv := httptest.NewServer(...)
// t.Cleanup(srv.Close)
}Always cleanup listeners, temp files, and chdir restores:
wd, _ := os.Getwd()
t.Cleanup(func() { _ = os.Chdir(wd) })Lab 1 — Golden encode
Workspace: ~/lab/90daysofx/01-go/day35
mkdir -p ~/lab/90daysofx/01-go/day35/testdata
cd ~/lab/90daysofx/01-go/day35
go mod init example.com/day35- Define a struct + pretty encoder.
- Write golden file.
- Test equality.
- Change encoder; see failure; update golden deliberately; review diff.
go test ./...Lab 2 — Update workflow
Implement -update flag or UPDATE_GOLDEN env. Document:
UPDATE_GOLDEN=1 go test ./...
git diff testdata/Treat the diff as a PR review surface.
Lab 3 — TempDir pipeline
Write a test that:
- Creates input file in
t.TempDir
- Runs a function writing output beside it
- Compares to golden content (not path)
- Leaves no residue outside temp
go test -race ./...Lab 4 — Non-determinism drill
Intentionally include time.Now() in output; watch golden flake. Fix by injecting a func() time.Time clock. Re-stabilize.
Common gotchas
| Gotcha | Fix |
|---|---|
| CRLF vs LF | Normalize or run on consistent OS; prefer \n |
| Map range order in custom encoders | Sort keys |
| Updating goldens blindly | Review every byte change |
| Storing secrets in testdata | Never |
| Huge binary goldens | Prefer text or semantic compares |
Checkpoint
- Golden test for an encoder
- Update workflow documented
- Uses
t.TempDir/t.Cleanupcorrectly
- Fixed a non-determinism issue
- Understands 1.26
ArtifactDirvs goldens
go testgreen
Commit
git add .
git commit -m "day35: golden files + TempDir/Cleanup labs"Write three personal gotchas before continuing.
Tomorrow
Day 36 — Concurrent tests / synctest: testing concurrent code without flakes—race detector, parallelism, and modern sync-test awareness.