Day 35 — Golden files & Cleanup

Updated

July 30, 2026

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.

Note

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 carefully

Common 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.TempDir over manual deletes
  • t.Cleanup runs 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 helper

For 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:

  1. Canonical JSON for a config struct
  2. Custom line protocol encode/decode
  3. 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
  1. Define a struct + pretty encoder.
  2. Write golden file.
  3. Test equality.
  4. 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:

  1. Creates input file in t.TempDir
  2. Runs a function writing output beside it
  3. Compares to golden content (not path)
  4. 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.Cleanup correctly
  • Fixed a non-determinism issue
  • Understands 1.26 ArtifactDir vs goldens
  • go test green

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.