452 lines
9.7 KiB
Markdown
452 lines
9.7 KiB
Markdown
# 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 |
|