Test Helpers, t.Helper and t.Cleanup in Go
Write reusable test helper functions with t.Helper for correct line numbers, clean up with t.Cleanup, and use t.TempDir, t.Setenv and testdata folders.
As tests grow, the same setup appears again and again: create a temp file, start a fake server, build a test user. Moving that into helper functions keeps tests short. The testing package has a few methods that make helpers behave well.
t.Helper for Correct Line Numbers
Without t.Helper(), a failure inside a helper reports the helper's line, which is the same for every caller and tells you nothing:
// file: helper_test.go
package money
import "testing"
func Add(a, b int) int { return a + b }
func assertEqual(t *testing.T, got, want int) {
t.Helper()
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
func TestAdd(t *testing.T) {
assertEqual(t, Add(2, 3), 5)
assertEqual(t, Add(-1, 1), 0)
}
t.Helper() marks the function as a helper, so a failure is reported at the line in TestAdd that called it. Put it as the first line of every test helper.
Pass t as the first parameter:
By convention, test helpers take t *testing.T (or testing.TB, which also works for benchmarks) as their first argument, and never return an error. They fail the test themselves with t.Fatalf, which keeps the calling test short.
t.Cleanup for Teardown
t.Cleanup(f) registers a function to run after the test and all its subtests finish. Helpers use it to undo their own setup, so callers do not need a defer:
// file: cleanup_test.go
package files
import (
"os"
"path/filepath"
"testing"
)
func writeTempConfig(t *testing.T, content string) string {
t.Helper()
dir, err := os.MkdirTemp("", "cfg")
if err != nil {
t.Fatalf("mkdir temp: %v", err)
}
t.Cleanup(func() { os.RemoveAll(dir) })
path := filepath.Join(dir, "app.conf")
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatalf("write config: %v", err)
}
return path
}
func TestReadConfig(t *testing.T) {
path := writeTempConfig(t, "mode=dev")
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if string(data) != "mode=dev" {
t.Errorf("got %q", data)
}
}
Cleanups run in reverse order of registration, like defer.
Built-in Helpers: TempDir, Setenv, Context
The testing package already has helpers for the most common cases, each cleaned up automatically:
// file: builtins_test.go
package builtins
import (
"os"
"path/filepath"
"testing"
)
func TestBuiltins(t *testing.T) {
dir := t.TempDir() // removed after the test
if err := os.WriteFile(filepath.Join(dir, "a.txt"), []byte("hi"), 0o644); err != nil {
t.Fatal(err)
}
t.Setenv("APP_MODE", "test") // restored after the test
if os.Getenv("APP_MODE") != "test" {
t.Error("env not set")
}
ctx := t.Context() // Go 1.24: cancelled just before cleanups run
if ctx.Err() != nil {
t.Error("context should be active during the test")
}
}
t.Setenv and parallel tests:
t.Setenv changes the environment for the whole process, so it cannot be used in a test that calls t.Parallel(). Go panics with testing: test using t.Setenv or t.Chdir can not use t.Parallel.
The testdata Folder
Fixed input files, such as sample JSON or expected output, go in a folder named testdata next to the tests. The go tool ignores it when building packages, and tests run with the package folder as the working directory, so a relative path works:
data, err := os.ReadFile("testdata/order.json")
Golden Files
For long outputs such as rendered HTML or reports, compare against a saved "golden" file instead of a string in the test. A common pattern adds an -update flag that rewrites the golden files when the output changes on purpose:
var update = flag.Bool("update", false, "update golden files")
func TestRender(t *testing.T) {
got := Render(sampleOrder)
golden := filepath.Join("testdata", "order.golden")
if *update {
os.WriteFile(golden, got, 0o644)
}
want, _ := os.ReadFile(golden)
if !bytes.Equal(got, want) {
t.Errorf("output differs from %s, run with -update if intended", golden)
}
}
Run go test -update after an intentional change, review the diff in git, and commit it.
Helpers that hide failures:
A helper that logs an error with t.Log instead of failing, or returns an error the caller ignores, lets broken setup pass silently. Helpers should fail the test with t.Fatalf when setup goes wrong.
Tidy tests:
Each test reads as a few lines of intent, setup lives in helpers marked with t.Helper(), and all temp files, env changes and servers are undone through t.Cleanup or the built-in helpers.
Summary
Start every test helper with t.Helper() so failures point at the caller, and register teardown with t.Cleanup. Use t.TempDir, t.Setenv and t.Context instead of writing your own versions, keep fixed inputs in testdata/, and use golden files with an -update flag for large expected outputs.