Go语言测试:单元测试与基准测试详解
1. Go测试简介
Go语言内置了强大的测试框架,使得编写单元测试和基准测试变得简单。Go的测试框架位于标准库的testing包中,配合go test命令可以方便地运行测试。
2. 单元测试基础
2.1 测试文件命名
Go语言的测试文件必须以_test.go结尾:
mymath/ ├── mymath.go └── mymath_test.go2.2 编写第一个测试
// mymath.go package mymath func Add(a, b int) int { return a + b } func Multiply(a, b int) int { return a * b }// mymath_test.go package mymath import ( "testing" ) func TestAdd(t *testing.T) { result := Add(2, 3) expected := 5 if result != expected { t.Errorf("Add(%d, %d) = %d; want %d", 2, 3, result, expected) } } func TestMultiply(t *testing.T) { result := Multiply(3, 4) expected := 12 if result != expected { t.Errorf("Multiply(%d, %d) = %d; want %d", 3, 4, result, expected) } }2.3 运行测试
# 运行所有测试 go test ./... # 运行指定测试 go test -v ./mymath # 运行指定函数测试 go test -v -run TestAdd ./mymath # 显示测试覆盖率 go test -cover ./mymath # 显示详细测试覆盖率 go test -coverprofile=coverage.out ./mymath go tool cover -html=coverage.out3. 测试函数格式
3.1 测试函数命名
测试函数必须以Test开头,后跟具体的测试名称:
func TestFunctionName(t *testing.T) { ... }3.2 表驱动测试
表驱动测试是一种常用的测试模式,通过数据表来组织测试用例:
func TestAdd(t *testing.T) { tests := []struct { name string a int b int expected int }{ {"positive numbers", 2, 3, 5}, {"negative numbers", -1, -2, -3}, {"mixed numbers", -1, 2, 1}, {"zero", 0, 5, 5}, {"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) } }) } }4. 测试标记与跳过
4.1 跳过测试
func TestNetworkCall(t *testing.T) { if testing.Short() { t.Skip("Skipping network test in short mode") } // 测试逻辑 }4.2 条件跳过
func TestExternalAPI(t *testing.T) { apiKey := os.Getenv("API_KEY") if apiKey == "" { t.Skip("Skipping test: API_KEY not set") } // 测试逻辑 }5. 子测试与测试组
5.1 子测试
使用t.Run创建子测试:
func TestStringManipulation(t *testing.T) { t.Run("UpperCase", func(t *testing.T) { result := strings.ToUpper("hello") if result != "HELLO" { t.Errorf("Expected HELLO, got %s", result) } }) t.Run("LowerCase", func(t *testing.T) { result := strings.ToLower("WORLD") if result != "world" { t.Errorf("Expected world, got %s", result) } }) t.Run("Reverse", func(t *testing.T) { result := Reverse("hello") if result != "olleh" { t.Errorf("Expected olleh, got %s", result) } }) }5.2 测试组
func TestDatabase(t *testing.T) { // 设置 db := setupTestDB(t) defer db.Close() // 测试用例组 tests := []struct { name string query string wantLen int }{ {"all users", "SELECT * FROM users", 10}, {"active users", "SELECT * FROM users WHERE active = true", 7}, {"admins", "SELECT * FROM users WHERE role = 'admin'", 3}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { rows, err := db.Query(tt.query) if err != nil { t.Fatalf("Query failed: %v", err) } defer rows.Close() count := 0 for rows.Next() { count++ } if count != tt.wantLen { t.Errorf("Got %d rows, want %d", count, tt.wantLen) } }) } }6. 基准测试
6.1 编写基准测试
基准测试函数以Benchmark开头:
func BenchmarkAdd(b *testing.B) { for i := 0; i < b.N; i++ { Add(2, 3) } } func BenchmarkMultiply(b *testing.B) { for i := 0; i < b.N; i++ { Multiply(3, 4) } }6.2 运行基准测试
# 运行基准测试 go test -bench=. # 运行指定基准测试 go test -bench=BenchmarkAdd # 显示内存分配统计 go test -bench=. -benchmem # 调整CPU数量 go test -bench=. -cpu=1,2,4,86.3 基准测试示例
func BenchmarkStringConcat(b *testing.B) { input := "hello" b.ResetTimer() for i := 0; i < b.N; i++ { result := "" for j := 0; j < 100; j++ { result += input } } } func BenchmarkStringBuilder(b *testing.B) { input := "hello" b.ResetTimer() for i := 0; i < b.N; i++ { var builder strings.Builder for j := 0; j < 100; j++ { builder.WriteString(input) } } }7. 示例函数
7.1 编写示例函数
示例函数以Example开头:
func ExampleAdd() { result := Add(2, 3) fmt.Println(result) // Output: 5 }7.2 无序输出示例
func ExampleSort() { nums := []int{3, 1, 4, 1, 5, 9} sort.Ints(nums) fmt.Println(nums) // Unordered output: [1 1 3 4 5 9] }8. 表格驱动测试的优势
| 优势 | 说明 |
|---|---|
| 代码复用 | 测试用例集中管理 |
| 易于扩展 | 新增用例只需添加表项 |
| 易于阅读 | 测试逻辑与数据分离 |
| 错误定位 | 每个子测试有独立名称 |
9. 测试辅助函数
9.1 错误辅助函数
func assertEqual(t *testing.T, got, want interface{}) { if got != want { t.Errorf("got %v, want %v", got, want) } } func assertNoError(t *testing.T, err error) { if err != nil { t.Errorf("unexpected error: %v", err) } } func assertNil(t *testing.T, got interface{}) { if got != nil { t.Errorf("got %v, want nil", got) } }9.2 测试数据辅助
func setupTestUser(id int) *User { return &User{ ID: id, Name: fmt.Sprintf("User %d", id), Email: fmt.Sprintf("user%d@example.com", id), } } func setupTestDB(t *testing.T) *sql.DB { db, err := sql.Open("sqlite", ":memory:") if err != nil { t.Fatalf("failed to open database: %v", err) } // 创建测试表 _, err = db.Exec(`CREATE TABLE users (id INT, name TEXT, email TEXT)`) if err != nil { db.Close() t.Fatalf("failed to create table: %v", err) } return db }10. 并行测试
10.1 并行测试
func TestParallel(t *testing.T) { tests := []struct { name string id int }{ {"test1", 1}, {"test2", 2}, {"test3", 3}, {"test4", 4}, } t.Parallel() for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() // 测试逻辑 time.Sleep(100 * time.Millisecond) }) } }11. 测试覆盖
11.1 查看测试覆盖率
# 生成覆盖率报告 go test -coverprofile=coverage.out ./... # 查看HTML覆盖率报告 go tool cover -html=coverage.out # 查看覆盖率百分比 go test -cover ./...11.2 覆盖率模式
# 设置覆盖率模式 go test -covermode=count ./... # 计数模式 go test -covermode=atomic ./... # 原子模式(多线程推荐) go test -covermode=set ./... # 设置模式12. 总结
Go语言的测试框架提供了完整的单元测试和基准测试功能。通过table-driven测试模式,可以组织清晰、易扩展的测试用例。在实际开发中,应该编写有意义的测试,利用子测试组织相关测试用例,通过基准测试优化性能,并定期检查测试覆盖率以确保代码质量。