Files
go-patterns/patterns/testing.md
T

671 lines
16 KiB
Markdown

# 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"` |