Advanced Testing
Overview
Table-driven tests, subtests, and test helpers make Go tests maintainable and expressive.
Fast feedback test loop
unit tests -------> deterministic concurrency tests -----> benchmarks
| | |
<1s target race-safe track regressions
Table-Driven Tests
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive", 2, 3, 5},
{"negative", -1, -2, -3},
{"zero", 0, 0, 0},
{"mixed", -1, 5, 4},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}Subtests
func TestAPI(t *testing.T) {
t.Run("GET", func(t *testing.T) {
t.Run("success", func(t *testing.T) { })
t.Run("not_found", func(t *testing.T) { })
})
t.Run("POST", func(t *testing.T) {
t.Run("valid", func(t *testing.T) { })
t.Run("invalid", func(t *testing.T) { })
})
}Test Helpers
func newTestServer(t *testing.T) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
}))
}
func TestClient(t *testing.T) {
server := newTestServer(t)
defer server.Close()
// Use server.URL
}Cleanup
func TestWithCleanup(t *testing.T) {
tmpDir := t.TempDir() // Auto-removed
f, _ := os.CreateTemp(tmpDir, "test")
t.Cleanup(func() {
f.Close()
})
}Deterministic Concurrency (testing/synctest)
For Go 1.26 codebases, adopt testing/synctest where timers, goroutines, and scheduling made tests flaky before.
import "testing/synctest"
func TestRateLimiter(t *testing.T) {
synctest.Run(func() {
rl := NewRateLimiter(1 * time.Second)
// Time is virtual and controlled within synctest.Run
if !rl.Allow() { t.Error("should allow") }
synctest.Wait() // Wait for internal goroutines
})
}Tip: keep business logic outside goroutine setup so synctest.Run stays small and focused.
Efficient Benchmarks (B.Loop)
Prefer B.Loop in modern toolchains for cleaner benchmark loops and fewer loop-control mistakes.
func BenchmarkAdd(b *testing.B) {
for b.Loop() {
Add(1, 2)
}
}Migration pattern:
// old
func BenchmarkParseOld(b *testing.B) {
for i := 0; i < b.N; i++ {
Parse(input)
}
}
// new
func BenchmarkParse(b *testing.B) {
for b.Loop() {
Parse(input)
}
}Go 1.26 Testing Upgrade Checklist
- Use
t.Cleanup,t.TempDir, andt.Setenvinstead of manual teardown. - Move flaky concurrency tests into
testing/synctest. - Standardize benchmark style on
B.Loop. - Run strict CI test command:
go test ./... -race -shuffle=on -count=1Parallel Subtests
func TestParallel(t *testing.T) {
tests := []struct{ name string }{{"a"}, {"b"}, {"c"}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Runs concurrently
})
}
}Benchmarks
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}go test -bench=.
go test -bench=. -benchmem # Include memorySummary
| Technique | Purpose |
|---|---|
| Table-driven | Test multiple cases |
| Subtests | Organize and filter |
t.Helper() |
Clean error reporting |
t.Cleanup() |
Guaranteed cleanup |
t.Parallel() |
Concurrent tests |