深色模式
Go 测试实践
概述
Go 的测试体系有个很实在的优点:默认就够用。只靠标准库 testing,已经能把单元测试、子测试、覆盖率、基准测试这几件日常工作做完整。
所以这篇文章不去堆一长串 API,而是围绕“项目里平时到底怎么写测试”来组织:先把文件和函数组织清楚,再看表驱动、子测试、清理逻辑和常用命令。
测试文件怎么放
Go 约定测试文件以 _test.go 结尾,例如:
calc.gocalc_test.go
测试函数以 TestXxx 命名,并接收 *testing.T:
go
func TestAdd(t *testing.T) {
got := Add(2, 3)
want := 5
if got != want {
t.Fatalf("Add(2, 3) = %d, want %d", got, want)
}
}这就是一个最小可运行的 Go 单元测试。
包名怎么选
测试文件有两种常见写法:
- 和被测代码同包,例如
package calc - 用外部测试包,例如
package calc_test
前者更适合直接测试包内未导出细节,后者更适合站在调用方视角验证公开 API。大多数业务项目里,两种写法都会出现,但一篇测试文件最好别来回混着用。
表驱动测试几乎是默认写法
只要同一个函数有多组输入输出,表驱动测试通常就是最顺手的组织方式。
go
func TestParsePage(t *testing.T) {
tests := []struct {
name string
input string
want int
wantErr bool
}{
{name: "empty", input: "", want: 1},
{name: "valid", input: "3", want: 3},
{name: "invalid", input: "abc", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parsePage(tt.input)
if (err != nil) != tt.wantErr {
t.Fatalf("unexpected error state: %v", err)
}
if got != tt.want {
t.Fatalf("got %d, want %d", got, tt.want)
}
})
}
}它的好处不是“写法高级”,而是:
- 用例集中
- 扩展新场景方便
- 错误输出更清楚
子测试适合把场景拆开
t.Run 不只是让代码好看一点,它还能把测试结果按场景拆开,失败时更容易定位。
例如上面的 empty、valid、invalid 就都是独立子测试。配合 go test -run,还能只跑某一部分。
sh
go test -run TestParsePage如果命名更细,也可以进一步过滤子测试名称。
辅助函数和 t.Helper 很值得用
测试一多,重复断言和准备逻辑很常见。可以抽成辅助函数:
go
func requireNoError(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}t.Helper() 的意义是告诉测试框架:这个函数是辅助函数。这样测试失败时,报错位置会指向真正调用它的那一行,而不是辅助函数内部。
资源清理优先用 t.Cleanup
如果测试里创建了临时文件、环境变量、数据库连接或 mock 服务,清理逻辑不要全靠手写 defer 拼凑,很多时候 t.Cleanup 更合适。
go
func TestWriteFile(t *testing.T) {
dir := t.TempDir()
old := os.Getenv("APP_ENV")
os.Setenv("APP_ENV", "test")
t.Cleanup(func() {
os.Setenv("APP_ENV", old)
})
// 在 dir 里执行测试
}TempDir 和 Cleanup 这两个组合,在文件系统和环境变量测试里特别顺手。
覆盖率和基准测试是两类不同问题
覆盖率
覆盖率回答的是:测试跑到了多少代码。
sh
go test -cover
go test -coverprofile=coverage.out
go tool cover -html=coverage.out覆盖率有用,但别把它当成质量的唯一指标。覆盖率高,不代表断言就一定有价值。
基准测试
基准测试回答的是:代码大概跑得有多快。
go
func BenchmarkParsePage(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = parsePage("123")
}
}运行:
sh
go test -bench=.它更适合用来比较实现方案,而不是追求一个抽象的“绝对性能分数”。
日常命令够用这几条
sh
go test ./...
go test -run TestParsePage ./...
go test -cover ./...
go test -bench=. ./...大多数项目里,把这几条用熟,比记一堆零散参数更重要。
测试里常见的几个坏习惯
- 只测正常路径,不测错误路径和边界条件
- 断言太弱,失败了也看不出问题
- 测试依赖全局状态,导致顺序一变就挂
- 一边测业务逻辑,一边偷偷做网络或数据库真实调用
测试最怕的不是数量少,而是看起来很多,实际一碰就碎。
