Go Testing Cheat Sheet
Covers writing unit tests and table-driven tests with the testing package, subtests, benchmarks, and running tests with the go test CLI.
Basic Test
The minimum shape of a Go unit test.
// math.gofunc Add(a, b int) int { return a + b }// math_test.gopackage mathpkgimport "testing"func TestAdd(t *testing.T) { got := Add(2, 3) want := 5 if got != want { t.Errorf("Add(2, 3) = %d; want %d", got, want) // Reports failure, continues test }}
Table-Driven Tests & Subtests
The idiomatic way to cover many cases with one test body.
func TestAddTable(t *testing.T) { cases := []struct { name string a, b int expected int }{ {"positive", 2, 3, 5}, {"negative", -1, -1, -2}, {"zero", 0, 0, 0}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { // Named subtest, runs independently got := Add(tc.a, tc.b) if got != tc.expected { t.Fatalf("got %d, want %d", got, tc.expected) // Stops this subtest immediately } }) }}
Benchmarks & Running Tests
Measuring performance and driving tests from the CLI.
func BenchmarkAdd(b *testing.B) { for i := 0; i < b.N; i++ { Add(2, 3) }}// go test ./... # Run all tests in the module// go test -v ./... # Verbose output// go test -run TestAdd # Run tests matching a regex// go test -cover ./... # Report code coverage// go test -bench=. # Run benchmarks
Concepts
Conventions the testing package relies on.
- _test.go suffix- Test files must end in _test.go and live in the same package (or pkgname_test)
- testing.T- t.Error/t.Errorf mark failure and continue; t.Fatal/t.Fatalf mark failure and stop the test
- Table-driven tests- Idiomatic Go pattern: a slice of input/expected cases run through one shared test body
- t.Run- Creates named subtests, each reported individually and runnable in isolation with -run
- testing.B- Used for benchmarks; b.N is set by the framework to get stable timing
- go test -cover- Reports statement coverage; -coverprofile=c.out produces a detailed report
- Mocks/stubs- Go favors small interfaces plus hand-written fakes over heavy mocking frameworks
TestMain, t.Cleanup & t.TempDir
Managing shared setup/teardown and scoped temporary resources.
func TestMain(m *testing.M) { // Runs once for the whole package: global setup before, teardown after setupDB() code := m.Run() teardownDB() os.Exit(code)}func TestWriteFile(t *testing.T) { dir := t.TempDir() // Auto-removed when the test (and subtests) finish f, err := os.Create(dir + "/out.txt") if err != nil { t.Fatal(err) } t.Cleanup(func() { // Runs in LIFO order, even after t.Fatal f.Close() }) if _, err := f.WriteString("data"); err != nil { t.Fatal(err) }}
Golden Files & testdata/
Comparing output against checked-in fixtures with an update flag.
var update = flag.Bool("update", false, "update golden files")func TestRender(t *testing.T) { got := Render(sampleInput) golden := filepath.Join("testdata", "render.golden") if *update { os.WriteFile(golden, got, 0644) } want, err := os.ReadFile(golden) if err != nil { t.Fatal(err) } if !bytes.Equal(got, want) { t.Errorf("Render() mismatch\ngot: %s\nwant: %s", got, want) }}// go test -run TestRender -update # regenerate testdata/render.golden// files/dirs under testdata/ are ignored by the go tool build system
Fuzz Testing (go test -fuzz)
Native fuzzing added in Go 1.18 to discover edge cases automatically.
func FuzzReverse(f *testing.F) { f.Add("hello") // Seed corpus entries f.Add("") f.Fuzz(func(t *testing.T, s string) { rev := Reverse(s) doubleRev := Reverse(rev) if s != doubleRev { t.Errorf("Reverse(Reverse(%q)) = %q", s, doubleRev) } if utf8.ValidString(s) && !utf8.ValidString(rev) { t.Errorf("Reverse produced invalid UTF-8 from %q", s) } })}// go test -fuzz=FuzzReverse -fuzztime=30s # run the fuzzer for 30s// crashers are saved under testdata/fuzz/FuzzReverse/
httptest for HTTP Handlers & Clients
Testing handlers without a real network listener, and mocking outbound calls.
func TestHandler(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/users/42", nil) rec := httptest.NewRecorder() UserHandler(rec, req) res := rec.Result() if res.StatusCode != http.StatusOK { t.Fatalf("status = %d, want 200", res.StatusCode) }}func TestClientAgainstFakeServer(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) })) defer srv.Close() resp, err := http.Get(srv.URL + "/health") if err != nil || resp.StatusCode != 200 { t.Fatalf("unexpected response: %v %v", resp, err) }}
Advanced Testing Toolbox
Lesser-known testing package features and go tool flags for production-grade suites.
- t.Parallel()- Marks a test to run concurrently with other parallel siblings; called at the top of the test func, before any t.Run
- -race- go test -race ./... enables the race detector; always run it in CI for concurrent code
- testing.Short()- Lets a test check `go test -short` and skip slow/integration paths via t.Skip
- t.Helper()- Marks a function as a test helper so failure line numbers point at the caller, not the helper
- go-cmp- google/go-cmp's cmp.Diff/cmp.Equal gives readable struct diffs, replacing brittle reflect.DeepEqual failures
- -coverprofile + go tool cover- go test -coverprofile=c.out then go tool cover -html=c.out renders an interactive per-line coverage report
- Example functions- func ExampleFoo() with a `// Output:` comment is compiled, run, and its stdout is checked automatically
- testing/quick- Lightweight property-based checks (quick.Check) predating fuzzing, still useful for pure functions
Use table-driven tests with t.Run subtests by default — it keeps cases readable, lets you re-run a single failing case with go test -run TestName/case_name, and scales cleanly as cases grow.