# Go Testing Patterns Patterns extracted from the Go standard library source code. --- ## 1. Table-Driven Tests with Subtests ### Source: `src/encoding/json/encode_test.go:405-430` ```go // src/encoding/json/encode_test.go:405-430 func TestUnsupportedValues(t *testing.T) { tests := []struct { CaseName in any }{ {Name(""), math.NaN()}, {Name(""), math.Inf(-1)}, {Name(""), math.Inf(1)}, {Name(""), pointerCycle}, {Name(""), pointerCycleIndirect}, {Name(""), mapCycle}, {Name(""), sliceCycle}, {Name(""), recursiveSliceCycle}, } for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { if _, err := Marshal(tt.in); err != nil { if _, ok := err.(*UnsupportedValueError); !ok { t.Errorf("%s: Marshal error:\n\tgot: %T\n\twant: %T", tt.Where, err, new(UnsupportedValueError)) } } else { t.Errorf("%s: Marshal error: got nil, want non-nil", tt.Where) } }) } } ``` ### Source: `src/encoding/json/encode_test.go:270-328` (with inputs and expected outputs) ```go // src/encoding/json/encode_test.go:270-328 func TestRoundtripStringTag(t *testing.T) { tests := []struct { CaseName in StringTag want string }{{ CaseName: Name("AllTypes"), in: StringTag{ BoolStr: true, IntStr: 42, UintptrStr: 44, StrStr: "xzbit", NumberStr: "46", }, want: `{ "BoolStr": "true", "IntStr": "42", ... }`, }, { CaseName: Name("StringDoubleEscapes"), in: StringTag{ StrStr: "\b\f\n\r\t\"\\", NumberStr: "0", }, want: `{...}`, }} for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { got, err := MarshalIndent(&tt.in, "", "\t") if err != nil { t.Fatalf("%s: MarshalIndent error: %v", tt.Where, err) } if got := string(got); got != tt.want { t.Fatalf("%s: MarshalIndent:\n\tgot: %s\n\twant: %s", tt.Where, ...) } // Verify round-trip var s2 StringTag if err := Unmarshal(got, &s2); err != nil { t.Fatalf("%s: Decode error: %v", tt.Where, err) } if !reflect.DeepEqual(s2, tt.in) { t.Fatalf("%s: Decode:\n\tinput: %s\n\tgot: %#v\n\twant: %#v", ...) } }) } } ``` ### Why Table-driven tests are Go's signature testing pattern: 1. **All cases visible in one place** — easy to add new cases 2. **t.Run creates subtests** — each case runs independently, can be filtered with `-run` 3. **Uniform structure** — input, expected output, test name 4. **Failures identify which case** — via the case name ### Template ```go func TestFoo(t *testing.T) { tests := []struct { name string input InputType want OutputType wantErr bool }{ {name: "basic", input: ..., want: ...}, {name: "empty", input: ..., want: ...}, {name: "error case", input: ..., wantErr: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := Foo(tt.input) if (err != nil) != tt.wantErr { t.Fatalf("Foo() error = %v, wantErr %v", err, tt.wantErr) } if got != tt.want { t.Errorf("Foo() = %v, want %v", got, tt.want) } }) } } ``` ### Anti-pattern ```go // DON'T: Separate test functions for each case func TestFoo_Basic(t *testing.T) { ... } func TestFoo_Empty(t *testing.T) { ... } func TestFoo_Error(t *testing.T) { ... } // 50 near-identical functions — hard to maintain // DON'T: Tests without names (hard to identify failures) tests := []struct{ in, want int }{{1, 2}, {3, 4}} for _, tt := range tests { // which test failed? index 0 or 1? } ``` --- ## 2. t.Helper() — Clean Error Reporting ### Source: `src/testing/testing.go:1415-1435` ```go // src/testing/testing.go:1415-1435 func (c *common) Helper() { if c.isSynctest { c = c.parent } c.mu.Lock() defer c.mu.Unlock() if c.helperPCs == nil { c.helperPCs = make(map[uintptr]struct{}) } var pc [1]uintptr n := runtime.Callers(2, pc[:]) if n == 0 { panic("testing: zero callers found") } if _, found := c.helperPCs[pc[0]]; !found { c.helperPCs[pc[0]] = struct{}{} c.helperNames = nil } } ``` ### Why When a test helper calls `t.Helper()`, failures report the **caller's** line number, not the helper's. Without it, every failure points to the helper function — useless for identifying which test case failed. ### Idiomatic Usage ```go func assertNoError(t *testing.T, err error) { t.Helper() // failures point to the caller, not this line if err != nil { t.Fatalf("unexpected error: %v", err) } } func assertEqual(t *testing.T, got, want any) { t.Helper() if !reflect.DeepEqual(got, want) { t.Errorf("got %v, want %v", got, want) } } ``` ### Anti-pattern ```go // DON'T: Forget t.Helper() in test helpers func checkResult(t *testing.T, got, want string) { // Missing t.Helper() if got != want { t.Errorf("got %q, want %q", got, want) // Error points HERE, not the actual test case — confusing } } ``` --- ## 3. t.Run() — Subtests and Test Organization ### Source: `src/testing/testing.go:2204-2260` ```go // src/testing/testing.go:2204-2215 func (t *T) Run(name string, f func(t *T)) bool { t.hasSub.Store(true) testName, ok, _ := t.tstate.match.fullName(&t.common, name) if !ok || shouldFailFast() { return true } // ... ctx, cancelCtx := context.WithCancel(context.Background()) t = &T{ common: common{ name: testName, parent: &t.common, // ... }, } go tRunner(t, f) // ... } ``` ### Why Subtests: 1. **Run in separate goroutines** — isolated from each other 2. **Have their own context** — `context.WithCancel(context.Background())` 3. **Can be filtered** — `go test -run TestFoo/subcase` 4. **Can run in parallel** — via `t.Parallel()` inside the subtest 5. **Share setup/teardown** — parent test's defer runs after all subtests ### Pattern: Setup/Teardown with Subtests ```go func TestDB(t *testing.T) { db := setupTestDB(t) // shared setup t.Cleanup(func() { db.Close() }) // runs after ALL subtests t.Run("Insert", func(t *testing.T) { // uses db }) t.Run("Query", func(t *testing.T) { // uses db }) } ``` ### Anti-pattern ```go // DON'T: Rely on test execution order func TestInsert(t *testing.T) { ... } // must run before TestQuery func TestQuery(t *testing.T) { ... } // depends on TestInsert's side effects // Tests should be independent! ``` --- ## 4. t.Parallel() — Concurrent Test Execution ### Source: `src/testing/testing.go:1912` ```go // src/testing/testing.go:1912 func (t *T) Parallel() { // ...marks test as parallel, pauses until parent completes } ``` ### Why `t.Parallel()` signals that this test can run concurrently with other parallel tests. The test pauses until its parent test function returns, then runs alongside other parallel subtests. ### Idiomatic Usage ```go func TestFoo(t *testing.T) { tests := []struct{ name string input int }{ {"small", 1}, {"large", 1000}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() // runs concurrently with other subtests result := expensiveOperation(tt.input) if result != expected { t.Errorf(...) } }) } } ``` ### Anti-pattern ```go // DON'T: Parallel tests that share mutable state var counter int func TestA(t *testing.T) { t.Parallel() counter++ // DATA RACE } func TestB(t *testing.T) { t.Parallel() counter++ // DATA RACE } // DON'T: Capture loop variable in Go < 1.22 without explicit copy for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() // In Go < 1.22: tt is shared — always sees last value // In Go >= 1.22: loop variables are per-iteration (fixed) }) } ``` --- ## 5. t.Cleanup() — Deterministic Teardown ### Source: `src/testing/testing.go:1439` ```go // src/testing/testing.go:1439-1442 // Cleanup registers a function to be called when the test (or subtest) // and all its subtests complete. Cleanup functions will be called in // last added, first called order. func (c *common) Cleanup(f func()) { ... } ``` ### Why `t.Cleanup` is like `defer` but tied to test lifecycle, not function scope. It runs after all subtests complete and works with parallel tests. ### Idiomatic Usage ```go func setupTestDB(t *testing.T) *sql.DB { t.Helper() db, err := sql.Open("sqlite3", ":memory:") if err != nil { t.Fatal(err) } t.Cleanup(func() { db.Close() }) return db } // Caller doesn't need to worry about cleanup: func TestQueries(t *testing.T) { db := setupTestDB(t) // automatically cleaned up // ... } ``` ### Anti-pattern ```go // DON'T: Return cleanup functions (easy to forget) func setupDB(t *testing.T) (*sql.DB, func()) { db := ... return db, func() { db.Close() } } // Caller must remember: db, cleanup := setupDB(t); defer cleanup() // DO: Use t.Cleanup inside the setup function ``` --- ## 6. t.TempDir() — Automatic Temp Directories ### Source: `src/testing/testing.go:1575` ```go // src/testing/testing.go:1575 func (c *common) TempDir() string { ... } ``` ### Why Creates a temp directory that's automatically removed when the test completes. No manual cleanup needed, no leftover test artifacts. ### Idiomatic Usage ```go func TestWriteConfig(t *testing.T) { dir := t.TempDir() // auto-cleaned path := filepath.Join(dir, "config.json") err := WriteConfig(path, myConfig) if err != nil { t.Fatal(err) } got, _ := os.ReadFile(path) // assert contents... } ``` ### Anti-pattern ```go // DON'T: Create temp dirs manually and forget cleanup func TestWrite(t *testing.T) { dir, _ := os.MkdirTemp("", "test") // forgot os.RemoveAll(dir) — test leaves garbage } ``` --- ## 7. testdata/ Directory ### Source: `src/net/http/testdata/`, `src/encoding/json/testdata/` (implicit — exists in the tree) ``` src/net/http/testdata/ ├── file ├── index.html └── style.css ``` ### Why The `testdata/` directory is special in Go: 1. **Ignored by the go tool** — not compiled as a package 2. **Available to tests** — accessed via relative path `"testdata/file.txt"` 3. **Committed to repo** — test fixtures live alongside the code 4. **Portable** — tests work without external dependencies ### Idiomatic Usage ```go func TestParse(t *testing.T) { input, err := os.ReadFile("testdata/input.json") if err != nil { t.Fatal(err) } want, err := os.ReadFile("testdata/expected.json") if err != nil { t.Fatal(err) } got := Parse(input) if !bytes.Equal(got, want) { t.Errorf("Parse mismatch") } } ``` ### Golden File Pattern ```go func TestOutput(t *testing.T) { got := generateOutput() golden := filepath.Join("testdata", t.Name()+".golden") if *update { // -update flag to regenerate os.WriteFile(golden, got, 0644) } want, _ := os.ReadFile(golden) if !bytes.Equal(got, want) { t.Errorf("output mismatch; run with -update to regenerate") } } ``` ### Anti-pattern ```go // DON'T: Embed large test fixtures as string literals var testInput = `{ "very": "long", "json": "string", // 500 lines... }` // DON'T: Depend on external URLs for test data func TestParse(t *testing.T) { resp, _ := http.Get("https://example.com/test.json") // flaky! } ``` --- ## 8. Error Message Formatting ### Source: `src/encoding/json/encode_test.go` (throughout) ```go // Pattern from encode_test.go:304 t.Fatalf("%s: MarshalIndent error: %v", tt.Where, err) // Pattern from encode_test.go:306-307 t.Fatalf("%s: MarshalIndent:\n\tgot: %s\n\twant: %s", tt.Where, got, want) // Pattern from encode_test.go:421-422 t.Errorf("%s: Marshal error:\n\tgot: %T\n\twant: %T", tt.Where, err, new(UnsupportedValueError)) ``` ### Why The stdlib follows a consistent error format: - **Context first** — where/what was being tested - **got/want on separate lines** — easy to diff visually - **Tab-indented** — aligns the comparison ### Convention ``` t.Errorf("FunctionName(%v) = %v, want %v", input, got, want) // or for complex values: t.Errorf("FunctionName(%v):\n\tgot: %v\n\twant: %v", input, got, want) ``` ### Anti-pattern ```go // DON'T: Vague error messages t.Error("failed") // what failed? what was expected? // DON'T: Only show the got value t.Errorf("got %v", got) // what was expected? // DON'T: Use assert libraries that hide the actual comparison assert.Equal(t, got, want) // when it fails: "not equal" — which is which? ``` --- ## 9. t.Fatal vs t.Error ### Convention across stdlib ```go // Fatal: test cannot continue meaningfully db, err := sql.Open("sqlite3", ":memory:") if err != nil { t.Fatal(err) // no point continuing without a database } // Error: test can continue, report all failures for _, tt := range tests { got := fn(tt.input) if got != tt.want { t.Errorf(...) // report and keep going } } ``` ### Why - `t.Fatal` / `t.Fatalf` — **stops the test immediately**. Use for setup failures where subsequent assertions are meaningless. - `t.Error` / `t.Errorf` — **reports failure, continues**. Use in loops to collect all failures at once. ### Anti-pattern ```go // DON'T: Fatal in loops (misses subsequent failures) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got != tt.want { t.Fatalf(...) // stops this subtest — fine in subtests actually } }) } // But without subtests: for _, tt := range tests { if got != tt.want { t.Fatalf(...) // stops ALL remaining cases! } } // DON'T: Error when you can't continue file, err := os.Open(path) if err != nil { t.Errorf("open: %v", err) // continues to use nil file — panic! } file.Read(...) ``` --- ## 10. Test Naming Conventions ### Source: All stdlib tests follow these patterns ```go // Function tests: TestFunctionName func TestMarshal(t *testing.T) { ... } // Method tests: TestTypeName_MethodName func TestEncoder_Encode(t *testing.T) { ... } // Behavior tests: TestDescription func TestOmitEmpty(t *testing.T) { ... } // Edge cases: TestFunctionName_EdgeCase func TestMarshal_NilSlice(t *testing.T) { ... } // Benchmarks: BenchmarkFunctionName func BenchmarkMarshal(b *testing.B) { ... } // Examples: ExampleFunctionName func ExampleMarshal() { // ... // Output: {"name":"Alice"} } ``` ### Why - Test names are used with `-run` flag for filtering - They appear in failure output — should be self-explanatory - Example functions become documentation (shown in godoc) - Subtests use `/` separator: `TestMarshal/NilSlice` ### Anti-pattern ```go // DON'T: Numbered tests func Test1(t *testing.T) { ... } func Test2(t *testing.T) { ... } // DON'T: Tests that don't start with Test func testHelper(t *testing.T) { ... } // won't be run! (lowercase 't') // DON'T: Overly verbose names func TestThatMarshalCorrectlyHandlesNilSliceInputAndReturnsNullJSON(t *testing.T) { ... } ``` --- ## Summary: Testing Best Practices | Pattern | When | |---------|------| | Table-driven tests | Multiple inputs for same logic | | t.Run subtests | Isolate cases, enable `-run` filtering | | t.Helper() | Every test helper function | | t.Parallel() | Independent tests, speed up suite | | t.Cleanup() | Resource teardown (replaces defer in helpers) | | t.TempDir() | Need filesystem for test | | testdata/ | External test fixtures | | Golden files | Complex expected output | | t.Fatal | Setup failures (can't continue) | | t.Error | Assertion failures (collect all) | | got/want format | `"got %v, want %v"` or `"\n\tgot: %v\n\twant: %v"` |