Helpers, Setup and Golden Files
t.Helper, t.Cleanup and t.TempDir keep test setup honest, and testdata holds the golden files you compare against.
t.Helper() · t.Cleanup(f func()) · t.TempDir() string · fixtures in testdata/Tests repeat setup: build a value, write a file, open a connection. Pulling that into a function keeps the tests readable, but it hides two things unless you say otherwise, and testing has a tool for each.
t.Helper and t.Cleanup
t.Helper(), called as the first line of a helper, tells the testing package that this function is not where the interesting failure is. Failure messages then point at the line that called the helper rather than the line inside it. Without it, every failure points at the same place and you cannot tell which case broke.
t.Cleanup(f) registers a function to run when the test finishes, including after t.Fatalf. It is the helper's version of defer: a defer inside a helper runs when the helper returns, which is too early, while a cleanup registered there runs when the test ends. Several cleanups run in reverse order of registration, last registered first, like stacked defers.
Temporary files and testdata
t.TempDir() returns the path of a new empty directory and removes it, and everything in it, when the test ends. The longhand is os.MkdirTemp plus defer os.RemoveAll, with the error checking that implies.
Fixed input and expected output belong in a directory named testdata, which the go tool ignores when building. A file there holding the exact expected output is called a golden file. Compare against it instead of pasting long text into the test, and regenerate it behind a flag when the output legitimately changes.
Example
// File: render_test.go — needs testdata/greeting.golden holding: Hello, Ada.
package render
import (
"os"
"path/filepath"
"testing"
)
// Render builds the greeting the golden file records.
func Render(name string) string { return "Hello, " + name + "." }
// golden reads a fixture and ends the test if it is missing.
func golden(t *testing.T, name string) string {
// Report failures at the caller's line, not this one.
t.Helper()
data, err := os.ReadFile(filepath.Join("testdata", name))
if err != nil {
t.Fatalf("read golden %s: %v", name, err)
}
return string(data)
}
func TestRender(t *testing.T) {
t.Cleanup(func() { t.Log("TestRender finished") })
// This directory is deleted when the test ends.
scratch := filepath.Join(t.TempDir(), "out.txt")
if err := os.WriteFile(scratch, []byte(Render("Ada")), 0o600); err != nil {
t.Fatalf("write %s: %v", scratch, err)
}
if got, want := Render("Ada"), golden(t, "greeting.golden"); got != want {
t.Errorf("Render = %q, want %q", got, want)
}
}
// Output:
// PASS
// ok render 0.003s
When to use it
- Share the ten lines of setup that every test in a package needs, without the failure messages all pointing at the setup.
- Compare generated output — HTML, JSON, a report — against a checked-in fixture instead of a string literal in the test.
- Write files during a test and have them removed afterwards, even when the test fails part-way through.
More examples
Regenerating a golden file behind a flag
Reach for this when the expected output is long: rendered HTML, a formatted report, generated SQL. The flag turns an intentional change into one command plus a reviewable diff in the fixture file. go test parses flags declared at package level, so no extra wiring is needed.
package render
import (
"flag"
"os"
"path/filepath"
"testing"
)
// go test -update rewrites the fixtures instead of comparing against them.
var update = flag.Bool("update", false, "rewrite the files in testdata")
func TestRenderGolden(t *testing.T) {
got := Render("Ada")
path := filepath.Join("testdata", "greeting.golden")
if *update {
if err := os.WriteFile(path, []byte(got), 0o600); err != nil {
t.Fatalf("update golden: %v", err)
}
}
want, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read golden: %v", err)
}
if got != string(want) {
t.Errorf("Render = %q, want %q (run go test -update to accept)", got, want)
}
}A helper that owns its own cleanup
This is the pattern that makes helpers worth writing: the caller gets a usable resource and never has to remember to release it. The same shape works for a database handle or a test server. Note the cleanup reports a close error with t.Errorf, so a resource leak shows up as a failure instead of silence.
func openForTest(t *testing.T, name string) *os.File {
t.Helper()
f, err := os.Create(filepath.Join(t.TempDir(), name))
if err != nil {
t.Fatalf("create %s: %v", name, err)
}
// defer here would close the file before the caller used it.
t.Cleanup(func() {
if err := f.Close(); err != nil {
t.Errorf("close %s: %v", name, err)
}
})
return f
}Setting an environment variable for one test
t.Setenv, added in Go 1.17, sets an environment variable and restores the previous value when the test ends, which os.Setenv does not. Use it to test configuration loading. A test that calls it may not call t.Parallel: that combination panics, because the variable is process-wide and another test running at the same time would see it.
func TestConfigFromEnv(t *testing.T) {
// Set for this test only; the old value is restored at the end.
t.Setenv("APP_PORT", "9090")
if got := os.Getenv("APP_PORT"); got != "9090" {
t.Fatalf("APP_PORT = %q, want 9090", got)
}
}
Discussion