# Testing and Benchmarking ## Table-Driven Tests ```go package math import "testing" func Add(a, b int) int { return a + b } func TestAdd(t *testing.T) { tests := []struct { name string a, b int expected int }{ {"positive numbers", 2, 3, 5}, {"negative numbers", -2, -3, -5}, {"mixed signs", -2, 3, 1}, {"zeros", 0, 0, 0}, {"large numbers", 1000000, 2000000, 3000000}, } 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 and Parallel Execution ```go func TestParallel(t *testing.T) { tests := []struct { name string input string want string }{ {"lowercase", "hello", "HELLO"}, {"uppercase", "WORLD", "WORLD"}, {"mixed", "HeLLo", "HELLO"}, } for _, tt := range tests { tt := tt // Capture range variable for parallel tests t.Run(tt.name, func(t *testing.T) { t.Parallel() // Run subtests in parallel result := strings.ToUpper(tt.input) if result != tt.want { t.Errorf("got %q, want %q", result, tt.want) } }) } } ``` ## Test Helpers and Setup/Teardown ```go func TestWithSetup(t *testing.T) { // Setup db := setupTestDB(t) defer cleanupTestDB(t, db) tests := []struct { name string user User }{ {"valid user", User{Name: "John", Email: "john@example.com"}}, {"empty name", User{Name: "", Email: "test@example.com"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := db.SaveUser(tt.user) if err != nil { t.Fatalf("SaveUser failed: %v", err) } }) } } // Helper function (doesn't show in stack trace) func setupTestDB(t *testing.T) *DB { t.Helper() db, err := NewDB(":memory:") if err != nil { t.Fatalf("failed to create test DB: %v", err) } return db } func cleanupTestDB(t *testing.T, db *DB) { t.Helper() if err := db.Close(); err != nil { t.Errorf("failed to close DB: %v", err) } } ``` ## Mocking with Interfaces ```go // Interface to mock type EmailSender interface { Send(to, subject, body string) error } // Mock implementation type MockEmailSender struct { SentEmails []Email ShouldFail bool } type Email struct { To, Subject, Body string } func (m *MockEmailSender) Send(to, subject, body string) error { if m.ShouldFail { return fmt.Errorf("failed to send email") } m.SentEmails = append(m.SentEmails, Email{to, subject, body}) return nil } // Test using mock func TestUserService_Register(t *testing.T) { mockSender := &MockEmailSender{} service := NewUserService(mockSender) err := service.Register("user@example.com") if err != nil { t.Fatalf("Register failed: %v", err) } if len(mockSender.SentEmails) != 1 { t.Errorf("expected 1 email sent, got %d", len(mockSender.SentEmails)) } email := mockSender.SentEmails[0] if email.To != "user@example.com" { t.Errorf("expected email to user@example.com, got %s", email.To) } } ``` ## Benchmarking ```go func BenchmarkAdd(b *testing.B) { for i := 0; i < b.N; i++ { Add(100, 200) } } // Benchmark with subtests func BenchmarkStringOperations(b *testing.B) { benchmarks := []struct { name string input string }{ {"short", "hello"}, {"medium", strings.Repeat("hello", 10)}, {"long", strings.Repeat("hello", 100)}, } for _, bm := range benchmarks { b.Run(bm.name, func(b *testing.B) { for i := 0; i < b.N; i++ { _ = strings.ToUpper(bm.input) } }) } } // Benchmark with setup func BenchmarkMapOperations(b *testing.B) { m := make(map[string]int) for i := 0; i < 1000; i++ { m[fmt.Sprintf("key%d", i)] = i } b.ResetTimer() // Don't count setup time for i := 0; i < b.N; i++ { _ = m["key500"] } } // Parallel benchmark func BenchmarkConcurrentAccess(b *testing.B) { var counter int64 b.RunParallel(func(pb *testing.PB) { for pb.Next() { atomic.AddInt64(&counter, 1) } }) } // Memory allocation benchmark func BenchmarkAllocation(b *testing.B) { b.ReportAllocs() // Report allocations for i := 0; i < b.N; i++ { s := make([]int, 1000) _ = s } } ``` ## Fuzzing (Go 1.18+) ```go func FuzzReverse(f *testing.F) { // Seed corpus testcases := []string{"hello", "world", "123", ""} for _, tc := range testcases { f.Add(tc) } f.Fuzz(func(t *testing.T, input string) { reversed := Reverse(input) doubleReversed := Reverse(reversed) if input != doubleReversed { t.Errorf("Reverse(Reverse(%q)) = %q, want %q", input, doubleReversed, input) } }) } // Fuzz with multiple parameters func FuzzAdd(f *testing.F) { f.Add(1, 2) f.Add(0, 0) f.Add(-1, 1) f.Fuzz(func(t *testing.T, a, b int) { result := Add(a, b) // Properties that should always hold if result < a && b >= 0 { t.Errorf("Add(%d, %d) = %d; result should be >= a when b >= 0", a, b, result) } }) } ``` ## Test Coverage ```go // Run tests with coverage: // go test -cover // go test -coverprofile=coverage.out // go tool cover -html=coverage.out func TestCalculate(t *testing.T) { tests := []struct { name string input int expected int }{ {"zero", 0, 0}, {"positive", 5, 25}, {"negative", -3, 9}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := Calculate(tt.input) if result != tt.expected { t.Errorf("Calculate(%d) = %d; want %d", tt.input, result, tt.expected) } }) } } ``` ## Race Detector ```go // Run with: go test -race func TestConcurrentAccess(t *testing.T) { var counter int var wg sync.WaitGroup // This will fail with -race if not synchronized for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() counter++ // Data race! }() } wg.Wait() } // Fixed version with mutex func TestConcurrentAccessSafe(t *testing.T) { var counter int var mu sync.Mutex var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() mu.Lock() counter++ mu.Unlock() }() } wg.Wait() if counter != 10 { t.Errorf("expected 10, got %d", counter) } } ``` ## Golden Files ```go import ( "os" "path/filepath" "testing" ) func TestRenderHTML(t *testing.T) { data := Data{Title: "Test", Content: "Hello"} result := RenderHTML(data) goldenFile := filepath.Join("testdata", "expected.html") if *update { // Update golden file: go test -update os.WriteFile(goldenFile, []byte(result), 0644) } expected, err := os.ReadFile(goldenFile) if err != nil { t.Fatalf("failed to read golden file: %v", err) } if result != string(expected) { t.Errorf("output doesn't match golden file\ngot:\n%s\nwant:\n%s", result, expected) } } var update = flag.Bool("update", false, "update golden files") ``` ## Integration Tests ```go // integration_test.go // +build integration package myapp import ( "testing" "time" ) func TestIntegration(t *testing.T) { if testing.Short() { t.Skip("skipping integration test in short mode") } // Long-running integration test server := startTestServer(t) defer server.Stop() time.Sleep(100 * time.Millisecond) // Wait for server client := NewClient(server.URL) resp, err := client.Get("/health") if err != nil { t.Fatalf("health check failed: %v", err) } if resp.Status != "ok" { t.Errorf("expected status ok, got %s", resp.Status) } } // Run: go test -tags=integration // Run short tests only: go test -short ``` ## Testable Examples ```go // Example tests that appear in godoc func ExampleAdd() { result := Add(2, 3) fmt.Println(result) // Output: 5 } func ExampleAdd_negative() { result := Add(-2, -3) fmt.Println(result) // Output: -5 } // Unordered output func ExampleKeys() { m := map[string]int{"a": 1, "b": 2, "c": 3} keys := Keys(m) for _, k := range keys { fmt.Println(k) } // Unordered output: // a // b // c } ``` ## Quick Reference | Command | Description | |---------|-------------| | `go test` | Run tests | | `go test -v` | Verbose output | | `go test -run TestName` | Run specific test | | `go test -bench .` | Run benchmarks | | `go test -cover` | Show coverage | | `go test -race` | Run race detector | | `go test -short` | Skip long tests | | `go test -fuzz FuzzName` | Run fuzzing | | `go test -cpuprofile cpu.prof` | CPU profiling | | `go test -memprofile mem.prof` | Memory profiling |