671 lines
16 KiB
Markdown
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"` |
|