Writing Tests
testing.T ยท Error ยท Fatal ยท LogTest files end in
_test.go and are excluded from normal builds. Test functions must be named TestXxx (capital X) and take a single *testing.T argument. Run with go test ./....
Basic test structure
TestXxx
// math_test.go package math_test import "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) } }
Error vs Fatal
t.Error / t.Fatal
// Errorf โ marks failed, test continues t.Errorf("got %v, want %v", got, want) // Fatalf โ marks failed, stops test immediately // Use when continuing would panic or give misleading failures t.Fatalf("setup failed: %v", err) // Log โ printed only on failure (or with -v) t.Logf("input was: %v", input) // Skip โ mark as skipped, not failed if testing.Short() { t.Skip("skipping in short mode") }
Running tests
go test
// Run all tests in current package go test . // Run all tests recursively go test ./... // Verbose output โ show all test names go test -v ./... // Run only tests matching a pattern go test -run TestAdd ./... go test -run TestAdd/case_name // Run with race detector go test -race ./... // Short mode โ skip slow tests go test -short ./...
Table-Driven Tests
idiomatic ยท slice of cases ยท t.RunTable-driven tests are the idiomatic Go pattern. Define all inputs and expected outputs upfront in a slice, then loop over them. Adding a new case is a single line โ no new test function needed.
Standard table-driven pattern
Table
func TestDivide(t *testing.T) { tests := []struct { name string a, b float64 want float64 wantErr bool }{ {"positive", 10, 2, 5, false}, {"negative", -6, 3, -2, false}, {"divide by 0", 1, 0, 0, true}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { got, err := Divide(tc.a, tc.b) if (err != nil) != tc.wantErr { t.Fatalf("wantErr=%v, got err=%v", tc.wantErr, err) } if !tc.wantErr && got != tc.want { t.Errorf("got %v, want %v", got, tc.want) } }) } }
Named test cases with a map
map variant
// map[string]struct gives free names, no separate name field needed func TestTrim(t *testing.T) { tests := map[string]struct{ input, want string }{ "leading spaces": {" hello", "hello"}, "trailing spaces": {"hello ", "hello"}, "both": {" hello ", "hello"}, "empty": {"", ""}, } for name, tc := range tests { t.Run(name, func(t *testing.T) { if got := strings.TrimSpace(tc.input); got != tc.want { t.Errorf("got %q, want %q", got, tc.want) } }) } }
t.Run Subtests
naming ยท parallel ยท filter
Nested subtests & parallel execution
t.Run
func TestUser(t *testing.T) { t.Run("create", func(t *testing.T) { // ... }) t.Run("delete", func(t *testing.T) { t.Run("existing", func(t *testing.T) { /* ... */ }) t.Run("not found", func(t *testing.T) { /* ... */ }) }) } // Run only delete subtests: // go test -run TestUser/delete // go test -run TestUser/delete/not_found (spaces become underscores)
t.Parallel โ run subtests concurrently
Parallel
func TestProcessItems(t *testing.T) { items := []string{"a", "b", "c"} for _, item := range items { item := item // capture loop variable (required pre-Go 1.22) t.Run(item, func(t *testing.T) { t.Parallel() // call immediately after t.Run body opens process(item) }) } // Parent waits for all parallel subtests before returning } // t.Parallel() signals the test runner to pause this subtest, // run the rest of the parent, then run all parallel subtests together.
When using
t.Parallel() in a loop, always shadow the loop variable inside the loop body (item := item) before Go 1.22. Otherwise every goroutine captures the same variable and reads the final value.Benchmarks
BenchmarkXxx ยท b.N ยท allocs ยท ResetTimerThe testing harness runs your loop repeatedly, increasing b.N until results are stable (usually ~1 second of wall time). b.N = 1 โ b.N = 100 โ b.N = 10000 โ b.N = 1234567 โ report Output: BenchmarkAdd-8 1234567 42.3 ns/op 0 allocs/op
Writing a benchmark
BenchmarkXxx
func BenchmarkJoin(b *testing.B) { words := []string{"go", "is", "fast"} for i := 0; i < b.N; i++ { strings.Join(words, " ") } } // Run benchmarks (not run by default) // go test -bench=. ./... // go test -bench=BenchmarkJoin -benchmem ./... // -benchmem adds: bytes/op and allocs/op columns // BenchmarkJoin-8 3842106 31.2 ns/op 24 B/op 1 allocs/op
ResetTimer โ exclude setup
ResetTimer
func BenchmarkSearch(b *testing.B) { // Setup โ don't measure this data := loadLargeDataset() b.ResetTimer() // clock starts here for i := 0; i < b.N; i++ { search(data, "target") } }
ReportAllocs & StopTimer
allocs
func BenchmarkEncode(b *testing.B) { b.ReportAllocs() // same as -benchmem for this benchmark payload := buildPayload() for i := 0; i < b.N; i++ { b.StopTimer() buf := resetBuffer() // don't measure reset b.StartTimer() json.NewEncoder(buf).Encode(payload) } }
Test Helpers
t.Helper ยท assert ยท requireMark shared assertion functions with
t.Helper(). Without it, failures point to the line inside the helper rather than the call site โ making failures hard to trace.
t.Helper() โ correct failure line numbers
t.Helper
// Without t.Helper(): failure points to line inside assertEqual // With t.Helper(): failure points to the assertEqual call site func assertEqual[T comparable](t *testing.T, got, want T) { t.Helper() if got != want { t.Errorf("got %v, want %v", got, want) } } func assertNoError(t *testing.T, err error) { t.Helper() if err != nil { t.Fatalf("unexpected error: %v", err) } } func TestSomething(t *testing.T) { result, err := doWork() assertNoError(t, err) // failure reported here โ assertEqual(t, result, "ok") // failure reported here โ }
Constructing test fixtures
Fixtures
// Return the thing under test and a cleanup function func newTestServer(t *testing.T) *Server { t.Helper() s, err := NewServer(":0") // :0 picks a free port if err != nil { t.Fatalf("newTestServer: %v", err) } t.Cleanup(func() { s.Close() }) return s } func TestEndpoint(t *testing.T) { s := newTestServer(t) // server closes automatically when test ends resp, err := http.Get(s.URL() + "/health") assertNoError(t, err) assertEqual(t, resp.StatusCode, 200) }
Setup & Teardown
TestMain ยท t.Cleanup ยท t.TempDir
t.Cleanup โ per-test teardown
t.Cleanup
// Registered cleanups run after the test (and its subtests) finish // Called in LIFO order โ mirrors defer semantics func TestWithDB(t *testing.T) { db := openTestDB(t) t.Cleanup(func() { db.Close() }) // test runs here // db.Close() called automatically on exit } // t.TempDir() creates a temp dir and registers its // removal as a cleanup automatically dir := t.TempDir() // dir removed when test ends
TestMain โ package-level setup
TestMain
// TestMain runs before any test in the package // Must call m.Run() and os.Exit with its result func TestMain(m *testing.M) { // Setup: start a shared test database db, err := startTestDB() if err != nil { log.Fatalf("db setup: %v", err) } testDB = db code := m.Run() // run all tests // Teardown db.Close() os.Exit(code) }
Prefer
t.Cleanup over defer inside tests when the cleanup is registered in a helper. defer runs at the end of the helper function (too early), while t.Cleanup runs at the end of the test that called the helper.Fakes & Interfaces
interface ยท fake ยท httptestGo's implicit interfaces make testing easy without a mocking library. Define an interface where the dependency is consumed, then implement a simple fake for tests.
Interface-based fakes
Fake
// Production code depends on an interface, not a concrete type type Mailer interface { Send(to, subject, body string) error } type UserService struct{ mailer Mailer } func (s *UserService) Register(email string) error { // ... create user ... return s.mailer.Send(email, "Welcome", "...") } // Fake for tests โ records calls, no real email sent type fakeMailer struct { sent []string } func (f *fakeMailer) Send(to, _, _ string) error { f.sent = append(f.sent, to) return nil } func TestRegister(t *testing.T) { m := &fakeMailer{} svc := &UserService{mailer: m} svc.Register("alice@example.com") if len(m.sent) != 1 || m.sent[0] != "alice@example.com" { t.Errorf("expected welcome email, got: %v", m.sent) } }
httptest โ test HTTP handlers without a server
httptest
import "net/http/httptest" func TestHealthHandler(t *testing.T) { req := httptest.NewRequest("GET", "/health", nil) w := httptest.NewRecorder() HealthHandler(w, req) resp := w.Result() if resp.StatusCode != http.StatusOK { t.Errorf("got %d, want 200", resp.StatusCode) } } // httptest.NewServer โ spin up a real HTTP server for integration tests ts := httptest.NewServer(myHandler()) defer ts.Close() resp, _ := http.Get(ts.URL + "/health")
Quick Reference
Cheat-sheet| Concept | Syntax / Command | Notes |
|---|---|---|
| Test function | func TestXxx(t *testing.T) | Must be in a _test.go file |
| Mark failed, continue | t.Errorf("got %v", got) | Test keeps running after this |
| Mark failed, stop | t.Fatalf("setup: %v", err) | Stops this test immediately |
| Log (on failure) | t.Logf("input: %v", x) | Only printed when test fails (or -v) |
| Skip | t.Skip("reason") | Marks skipped, not failed |
| Short mode check | testing.Short() | True when -short flag is set |
| Subtest | t.Run("name", func(t *testing.T) {}) | Isolated; filterable by name |
| Parallel subtest | t.Parallel() | Call at start of t.Run body |
| Cleanup | t.Cleanup(func() { โฆ }) | Runs after test ends; LIFO |
| Temp dir | t.TempDir() | Auto-deleted on test exit |
| Helper marker | t.Helper() | Failure line points to call site |
| Benchmark | func BenchmarkXxx(b *testing.B) | Loop body runs b.N times |
| Reset timer | b.ResetTimer() | Exclude setup from measurement |
| Report allocs | b.ReportAllocs() | Same as -benchmem for this bench |
| Package setup | func TestMain(m *testing.M) | Must call os.Exit(m.Run()) |
| Run all tests | go test ./... | Recursive |
| Verbose | go test -v ./... | Print all test names |
| Filter by name | go test -run TestFoo/subname | Regex matched; spaces โ underscores |
| Run benchmarks | go test -bench=. -benchmem ./... | Not run by default |
| Race detector | go test -race ./... | Detects data races; add to CI |
| Coverage | go test -cover ./... | -coverprofile=c.out for detail |
| HTTP recorder | httptest.NewRecorder() | Captures handler response |
| HTTP test server | httptest.NewServer(h) | Real server; call defer ts.Close() |