学完这篇,你能独立给 Go 项目写出可维护的单元测试和基准测试,并会用 go test 的一整套参数把覆盖率、性能数据跑出来。
第一步:先跑通第一个测试
Go 的测试不需要任何第三方框架,标准库 testing 就够。规则只有三条:
- 文件名必须以
_test.go 结尾;
- 函数名必须是
TestXxx(Xxx 首字母大写);
- 参数固定为
t *testing.T。
新建 calc.go 和 calc_test.go:
// calc.go
package calc
func Add(a, b int) int { return a + b }
// calc_test.go
package calc
import "testing"
func TestAdd(t *testing.T) {
if got := Add(1, 2); got != 3 {
t.Errorf("Add(1,2) = %d, want 3", got)
}
}
在项目根目录执行:
go test ./... # 跑所有包的测试
go test -v ./calc # 看每个用例的执行明细
注意:t.Errorf 记录错误但继续执行,t.Fatalf 立即终止当前函数。断言失败后如果后面的代码会 panic,必须用 Fatalf。另外 go test 有结果缓存,加了 -v 或改代码才会重跑,想强制跑加 -count=1。
第二步:用表驱动测试覆盖多组输入
Go 社区最推荐的写法是表驱动(table-driven),一次覆盖十几种情况:
func TestAdd(t *testing.T) {
cases := []struct {
name string
a, b int
want int
}{
{"正数", 1, 2, 3},
{"负数", -1, -2, -3},
{"零值", 0, 0, 0},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if got := Add(c.a, c.b); got != c.want {
t.Errorf("Add(%d,%d) = %d, want %d", c.a, c.b, got, c.want)
}
})
}
}
t.Run 会生成子测试,输出里能看到 TestAdd/负数 这样的名字,定位问题非常快,也可以用 -run TestAdd/负数 单独跑某一条。
注意:循环变量 c 在 Go 1.22 之前是所有迭代共用的,闭包里直接引用会踩坑。老版本要写 c := c 重新声明一份,或者升级到 Go 1.22+。
第三步:看覆盖率,找没测到的分支
go test -cover ./... # 终端直接看百分比
go test -coverprofile=cover.out ./... # 输出明细文件
go tool cover -html=cover.out # 浏览器里高亮未覆盖代码
go tool cover -func=cover.out # 按函数列出覆盖率
绿色是已覆盖,红色是没跑到。别盲目追求 100%,重点盯住核心业务逻辑和边界分支(空切片、错误返回、超时)。
第四步:写基准测试量性能
基准测试函数名是 BenchmarkXxx,参数是 b *testing.B,被测代码必须放在 for i := 0; i < b.N; i++ 循环里,b.N 由框架自动调整到测量稳定为止:
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(i, i+1)
}
}
运行与常用参数:
go test -bench=. -benchmem ./... # 跑全部基准 + 显示内存分配
go test -bench=Add -benchtime=3s ./calc # 每条跑 3 秒
go test -bench=. -count=5 ./... # 重复 5 次看波动
-benchmem 会多输出 B/op(每次操作字节数)和 allocs/op(分配次数),优化内存时主要看这两个指标。
注意:循环外的准备工作(构造大切片、读文件)要放在循环之前,然后调用 b.ResetTimer() 把计时归零;被测函数如果返回值没被使用,编译器可能把整个调用优化掉,要把结果赋给一个包级变量再对比。
第五步:并行测试与并发基准
耗时长的用例可以并行:
func TestSlow(t *testing.T) {
t.Parallel() // 声明可并行,配合 go test -parallel 4
// ...
}
并发场景的基准用 b.RunParallel:
func BenchmarkConcurrent(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Add(1, 2)
}
})
}
第六步:几个提升可维护性的工具函数
t.Helper():写在自定义断言函数里,报错时栈信息指向真正的调用行,而不是断言函数内部;
t.Cleanup(fn):注册清理逻辑,测试结束自动执行,比手动 defer 更安全;
t.TempDir():拿到一个测试结束自动删除的临时目录,适合测文件读写;
TestMain(m *testing.M):需要在所有测试前后做全局初始化(连数据库、起容器)时用,结尾必须 os.Exit(m.Run());
testing.Short():配合 go test -short 跳过耗时的集成测试。
注意:测试包名写成 package calc_test 可以强制只测试导出 API,能发现"因为同包才能访问内部符号"的假测试;但需要访问未导出函数时,只能和源码同包。
小结
- 文件名
_test.go、函数名 TestXxx/BenchmarkXxx/ExampleXxx,是 testing 包识别的唯一依据;
- 多组输入用表驱动 +
t.Run,失败信息带 want/got 才好排查;
-cover 系列看覆盖,-bench=. -benchmem 看性能与内存,-count=1 关缓存;
- 基准测试的计时循环里只放被测代码,准备阶段用
b.ResetTimer() 排除;
t.Helper、t.Cleanup、t.TempDir、TestMain 这四个工具能让测试代码干净很多。