Go 测试
字数
1471 字
阅读时间
7 分钟
测试一般和被测代码在同一包中,测试文件命名以 _test.go 结尾,测试函数命名以 Test 开头。
测试结构
表驱动测试(Table-driven tests)
- 结构:用切片定义测试用例,包含输入、期望输出、错误场景。go
func TestDivide(t *testing.T) { tests := []struct { a, b int want int wantErr bool }{ {4, 2, 2, false}, {1, 0, 0, true}, } for _, tt := range tests { got, err := Divide(tt.a, tt.b) if (err != nil) != tt.wantErr { t.Errorf("Divide(%d, %d) error = %v, want %v", tt.a, tt.b, err, tt.wantErr) } } }
子测试(Subtests)
结构
使用 t.Run() 为每个测试用例创建子测试,便于统一执行启动和清理的代码。
go
func TestFoo(t *testing.T) {
// <setup code>
t.Run("A=1", func(t *testing.T) { ... })
t.Run("A=2", func(t *testing.T) { ... })
t.Run("B=1", func(t *testing.T) { ... })
// <tear-down code>
}避免使用 idx 来指示错误位置,而是使用 subtest 名称。
go
// BAD:
for i, d := range tests {
if strings.ToUpper(d.input) != d.want {
t.Errorf("Failed on case #%d", i)
}
}碎碎念
躺枪了,我就这样写过。但是有时候序号比名字更好找,有位置信息。
命名
清晰描述测试场景,避免特殊字符(如斜杠),支持命令行过滤。
go
func TestTranslate(t *testing.T) {
data := []struct {
name, desc, srcLang, dstLang, srcText, wantDstText string
}{
{
name: "hu=en_bug-1234",
desc: "regression test following bug 1234. contact: cleese",
srcLang: "hu",
srcText: "cigarettát és egy öngyújtót kérek",
dstLang: "en",
wantDstText: "cigarettes and a lighter please",
}, // ...
}
for _, d := range data {
t.Run(d.name, func(t *testing.T) {
got := Translate(d.srcLang, d.dstLang, d.srcText)
if got != d.wantDstText {
t.Errorf("%s\nTranslate(%q, %q, %q) = %q, want %q",
d.desc, d.srcLang, d.dstLang, d.srcText, got, d.wantDstText)
}
})
}
}执行
bash
go test -run '' # Run 所有测试
go test -run Foo # Run Foo 开头的测试
go test -run Foo/A= # Run Foo 开头里面名字是 A= 开头的子测试
go test -run /A=1 # Run 所有测试里 A=1 的子测试测试的并发
默认的 TestXXX 函数是串行执行,可以通过t.Parallel 函数让它们并发。而子测试默认是并发执行的,无需额外设置。
不同测试报告内容
性能测试(Benchmarks)
结构:使用
testing.B运行基准测试,避免使用time.Sleep()等阻塞操作。命名:清晰描述测试场景,避免特殊字符(如斜杠),支持命令行过滤。
gofunc BenchmarkFoo(b *testing.B) { for i:= 0; i< b.N; i++ { Foo() } }执行:可以用
b.N来调整循环次数,Go 会自动调整。bashgo test -bench .
覆盖率(Coverage)
- 要求:测试覆盖率达到 80% 以上。
- 命令:
go test -cover查看覆盖率报告。
测试的最佳实践
使用 t.Errorf 而非 t.Fatalf
- 区别:
t.Fatalf会导致测试立即终止,而t.Errorf仅记录错误信息,测试继续执行。 - 使用场景:
t.Fatalf用于严重错误,如测试环境配置错误,t.Errorf用于普通错误,如预期结果不匹配。
使用 t.Log
- 只有当测试失败时,
t.Log才会输出。
使用 t.Helper
- 场景:在测试函数中使用
t.Helper()标记当前函数为辅助函数,辅助函数不会被测试框架视为测试函数,避免在测试函数中出现冗余的错误信息。 - 示例:go
func TestFoo(t *testing.T) { if err:= Foo(); err!= nil { t.Helper() t.Errorf("Foo() failed: %v", err) } }
使用 t.TempDir
- 场景:在测试函数中使用
t.TempDir()创建临时目录,测试完成后自动删除。 - 示例:go
func TestFoo(t *testing.T) { dir:= t.TempDir() // ... }
使用 t.Cleanup
- 场景:在测试函数中使用
t.Cleanup()注册清理函数,测试完成后自动执行。 会比 defer 更可靠。 - 示例:go
func TestFoo(t *testing.T) { t.Cleanup(func() { //... }) }
使用 %q 代替 "%s"
- 格式:使用
%q格式化字符串,避免因字符串包含特殊字符(如换行符)导致的错误。 - 示例:go
t.Errorf("want %q, got %q", want, got)
断言与比较
Testify
Testify 测试框架提供了断言和模拟。
goassert.Equal(t, want, got, "msg")require和assert区别:require会在测试失败时立即终止测试,而assert会继续执行测试。争议:规范建议不要使用断言库,而是使用
cmp库进行比较,t.Errorf输出错误信息。goif !cmp.Equal(got, want) { t.Errorf("result mismatch: got %v, want %v", got, want) }TIP
尽管规范不鼓励,但我个人仍然觉得 assert 用起来显得代码简洁。
diffs、比较
第三方库增强比较,输出 diffs: 对于指针字段,直接使用
==比较会比较指针地址,而不是字段值。 虽然可以使用reflect.DeepEqual进行比较,但不支持定制化。 一些推荐的库:第三方库增强打印: 变量包含指针字段时,直接打印会打印指针地址,而不是字段值。可以用以下库:
- go-spew: 可以直观打印结构体、map、slice 等数据结构。
gospew.Dump(got)错误的比较:使用
errors.Is比较错误,避免字符串匹配。
mock
通过 mock 可以模拟依赖,避免依赖外部服务。
- 官方的 mock 框架
- Testify 也提供了 mock 功能。
