Golang 单元测试指南
Go 语言自带了一个轻量级的测试框架
testing和go test命令,这使得编写和执行测试变得非常简单和高效。本文将介绍如何在 Go 中编写单元测试、表驱动测试以及基准测试。
一、基础单元测试
在 Go 中,测试文件通常以 _test.go 结尾,并与被测试的代码放在同一个包中。测试函数必须以 Test 开头,并接收一个 *testing.T 类型的参数。
假设我们有一个简单的加法函数 Add:
// calc.go
package calc
func Add(a, b int) int {
return a + b
}
对应的测试文件 calc_test.go 如下:
// calc_test.go
package calc
import "testing"
func TestAdd(t *testing.T) {
result := Add(1, 2)
expected := 3
if result != expected {
t.Errorf("Add(1, 2) = %d; expected %d", result, expected)
}
}
运行测试很简单,只需在终端执行:
go test
二、表驱动测试 (Table-Driven Tests)
Go 社区非常推崇“表驱动测试”。这种方式将测试数据(输入和预期输出)定义在一个切片或数组中,然后遍历这个集合执行测试。这样可以轻松添加新的测试用例,且代码结构清晰。
func TestAddTableDriven(t *testing.T) {
// 定义测试用例结构
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 1, 2, 3},
{"negative numbers", -1, -2, -3},
{"mixed numbers", -1, 1, 0},
{"zero", 0, 0, 0},
}
// 遍历测试用例
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; expected %d", tt.a, tt.b, result, tt.expected)
}
})
}
}
使用 t.Run 可以创建子测试,这样在运行 go test -v 时,可以看到每个子测试的名称和结果。
三、TestMain:测试的 Setup 和 Teardown
有时候我们需要在所有测试运行之前进行一些初始化操作(如连接数据库),或在测试结束后清理资源。这时可以使用 TestMain。
func TestMain(m *testing.M) {
// setup code...
println("Before all tests")
// 运行测试
code := m.Run()
// teardown code...
println("After all tests")
// 退出
os.Exit(code)
}
注意:如果有 TestMain,测试不会自动运行,必须显式调用 m.Run()。
四、基准测试 (Benchmark)
除了单元测试,Go 还内置了基准测试框架,用于测试代码的性能。基准测试函数必须以 Benchmark 开头,接收 *testing.B 参数。
func BenchmarkAdd(b *testing.B) {
// b.N 会被框架自动调整,直到运行时间足够长
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}
运行基准测试:
go test -bench=.
如果想要查看内存分配情况,可以添加 -benchmem 参数:
go test -bench=. -benchmem
五、 并发测试 (Parallel Testing)
如果你的测试用例之间相互独立,可以使用 t.Parallel() 让它们并发运行,从而显著减少测试的总耗时。
func TestParallel(t *testing.T) {
t.Parallel() // 标记该测试可以与其他测试并发运行
// ... actual test logic ...
}
六、 模糊测试 (Fuzzing)
从 Go 1.18 开始,Go 原生支持模糊测试。Fuzzing 可以自动生成随机输入来查找代码中的边界情况或 crash。
func FuzzAdd(f *testing.F) {
// 添加种子语料库 (Seed Corpus)
f.Add(1, 2)
f.Fuzz(func(t *testing.T, a int, b int) {
// 运行被测函数
result := Add(a, b)
// 验证性质(例如:加法满足交换律)
if result != Add(b, a) {
t.Errorf("Commutative property failed: %d + %d != %d + %d", a, b, b, a)
}
})
}
运行 Fuzz 测试:go test -fuzz=Fuzz
七、 测试辅助函数 (Test Helpers)
当你提取公共的测试逻辑到辅助函数时,如果测试失败,错误日志通常会指向辅助函数内部,而不是调用它的测试用例。使用 t.Helper() 可以标记当前函数为辅助函数,报错时会展示调用者的行号。
func assertEqual(t *testing.T, got, want int) {
t.Helper() // 标记为辅助函数
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
八、常用测试命令参数
go test -v: 显示详细输出,包括每个测试用例的名称。go test -cover: 显示测试覆盖率。go test -run <RegExp>: 只运行匹配正则表达式的测试函数。go test -bench .: 运行当前包下的所有基准测试。go test -benchmem .: 在运行基准测试时显示内存分配统计信息。go test -race: 数据竞争检测