Testing and Benchmarking
Table-Driven Tests
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
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
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: "[email protected]"}},
{"empty name", User{Name: "", Email: "[email protected]"}},
}
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
// 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("[email protected]")
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 != "[email protected]" {
t.Errorf("expected email to [email protected], got %s", email.To)
}
}
Benchmarking
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+)
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
// 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
// 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
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
// 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
// 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 |