jee said
Go语言单元测试
Go语言内置了强大的测试框架,使得编写和运行测试变得简单而高效。本文将介绍Go语言的单元测试最佳实践。
基本测试
测试文件命名
Go语言的测试文件需要以_test.go结尾,并放在与被测试文件相同的包中。
// 被测试文件:math.go
// 测试文件:math_test.go基本测试结构
// math.go
package math
func Add(a, b int) int {
return a + b
}// math_test.go
package math
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; 期望 %d", result, expected)
}
}运行测试
# 运行所有测试
go test
# 运行指定包的测试
go test ./math
# 运行指定测试
go test -run TestAdd
# 显示详细输出
go test -v表驱动测试
表驱动测试是Go语言中常见的测试模式,可以减少重复代码。
基本表驱动测试
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"正数相加", 2, 3, 5},
{"负数相加", -2, -3, -5},
{"零相加", 0, 0, 0},
{"混合相加", 5, -3, 2},
}
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; 期望 %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}复杂表驱动测试
type DivideTest struct {
name string
a, b int
expected int
expectError bool
}
func TestDivide(t *testing.T) {
tests := []DivideTest{
{"正常除法", 10, 2, 5, false},
{"除以零", 10, 0, 0, true},
{"负数除法", -10, 2, -5, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Divide(tt.a, tt.b)
if tt.expectError {
if err == nil {
t.Errorf("期望错误但没有错误")
}
} else {
if err != nil {
t.Errorf("不期望错误但得到: %v", err)
}
if result != tt.expected {
t.Errorf("Divide(%d, %d) = %d; 期望 %d",
tt.a, tt.b, result, tt.expected)
}
}
})
}
}测试辅助函数
t.Helper()
func assertEqual(t *testing.T, got, expected int) {
t.Helper() // 标记为辅助函数
if got != expected {
t.Errorf("期望 %d,得到 %d", expected, got)
}
}
func TestAdd(t *testing.T) {
assertEqual(t, Add(2, 3), 5)
assertEqual(t, Add(-2, -3), -5)
}自定义辅助函数
// 辅助函数文件:testutil.go
package math
func AssertEqual(t *testing.T, got, expected int) {
t.Helper()
if got != expected {
t.Errorf("期望 %d,得到 %d", expected, got)
}
}
// 测试文件:math_test.go
package math
func TestAdd(t *testing.T) {
AssertEqual(t, Add(2, 3), 5)
}测试覆盖率
生成覆盖率报告
# 生成覆盖率报告
go test -coverprofile=coverage.out
# 查看覆盖率
go tool cover -html=coverage.out设置覆盖率阈值
# 设置最低覆盖率要求
go test -coverprofile=coverage.out -covermode=atomic
# 检查覆盖率是否达标
go tool cover -func=coverage.out | grep total覆盖率过滤
// 在测试文件中添加构建标签
// +build !integration
func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("期望 5,得到 %d", result)
}
}# 排除集成测试
go test -shortMock和Stub
接口Mock
// 定义接口
type Database interface {
GetUser(id int) (*User, error)
}
// Mock实现
type MockDatabase struct {
users map[int]*User
}
func (m *MockDatabase) GetUser(id int) (*User, error) {
user, ok := m.users[id]
if !ok {
return nil, fmt.Errorf("用户不存在")
}
return user, nil
}
// 使用Mock测试
func TestGetUser(t *testing.T) {
mockDB := &MockDatabase{
users: map[int]*User{
1: {ID: 1, Name: "张三"},
},
}
user, err := mockDB.GetUser(1)
if err != nil {
t.Errorf("获取用户失败: %v", err)
return
}
if user.Name != "张三" {
t.Errorf("期望用户名 张三,得到 %s", user.Name)
}
}函数Stub
// 原始函数
var getCurrentTime = time.Now
// Stub函数
func setCurrentTime(t time.Time) {
getCurrentTime = func() time.Time {
return t
}
}
// 测试中使用Stub
func TestTimeBasedLogic(t *testing.T) {
// 设置固定时间
setCurrentTime(time.Date(2024, 1, 1, 0, 0, 0, time.UTC))
// 测试基于时间的逻辑
result := someTimeBasedFunction()
// 验证结果
if result != expected {
t.Errorf("期望 %s,得到 %s", expected, result)
}
}基准测试
基本基准测试
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(i, i+1)
}
}# 运行基准测试
go test -bench=.
# 运行指定基准测试
go test -bench=BenchmarkAdd基准测试参数
func BenchmarkAdd(b *testing.B) {
b.ReportAllocs() // 报告内存分配
for i := 0; i < b.N; i++ {
Add(i, i+1)
}
}# 显示内存分配信息
go test -bench=. -benchmem并发测试
并发测试示例
func TestConcurrentAccess(t *testing.T) {
counter := 0
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter++
}()
}
wg.Wait()
if counter != 100 {
t.Errorf("期望计数器为100,得到 %d", counter)
}
}并发基准测试
func BenchmarkConcurrentAdd(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Add(1, 2)
}
})
}子测试
子测试结构
func TestCalculator(t *testing.T) {
t.Run("加法", func(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("期望 5,得到 %d", result)
}
})
t.Run("减法", func(t *testing.T) {
result := Subtract(5, 3)
if result != 2 {
t.Errorf("期望 2,得到 %d", result)
}
})
}运行子测试
# 运行特定子测试
go test -run TestCalculator/加法
# 运行所有子测试
go test -run TestCalculator/测试最佳实践
- 使用表驱动测试:减少重复代码,提高可维护性
- 测试边界条件:测试空值、零值、最大值等边界情况
- 使用辅助函数:减少测试代码的重复
- 保持测试独立:每个测试应该独立运行,不依赖其他测试
- 使用有意义的测试名称:测试名称应该清楚描述测试内容
- 测试错误情况:不仅要测试正常情况,还要测试错误情况
- 保持测试快速:单元测试应该快速运行
- 使用Mock隔离依赖:使用Mock对象隔离外部依赖
总结
Go语言的测试框架简洁而强大,通过合理的测试策略和最佳实践,可以编写出高质量、可维护的测试代码。掌握Go语言的测试技巧对于提高代码质量至关重要。