测试与工程化
本教程共 80 篇 · 第 79 篇 · 更新于 2026-07-27 · 约 9 分钟阅读
79. 测试与工程化
本节目标:学会写单元测试和表驱动测试,跑 Benchmark 性能测试,了解模糊测试,掌握 go vet 和 go fix 工具。
第一个测试
Go 测试文件以 _test.go 结尾,和被测代码同目录。函数名以 Test 开头,参数是 *testing.T:
// 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) {
got := Add(1, 2)
want := 3
if got != want {
t.Errorf("Add(1, 2) = %d, want %d", got, want)
}
}
运行测试:
go test # 当前包
go test ./... # 所有包
go test -v # 显示详细输出
go test -run TestAdd # 只跑指定测试
测试失败的方式
// 报告失败,继续执行
t.Errorf("got %d, want %d", got, want)
// 报告失败,立即停止当前测试
t.Fatalf("got %d, want %d", got, want)
// 跳过测试
t.Skip("跳过:需要数据库环境")
表驱动测试
Go 最经典的测试模式—把用例放在表里循环跑:
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"正数", 1, 2, 3},
{"负数", -1, -2, -3},
{"零", 0, 0, 0},
{"混合", -1, 1, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.want {
t.Errorf("Add(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.want)
}
})
}
}
t.Run 创建子测试,每个用例有名字,失败时能看清是哪个挂了:
go test -v
# --- PASS: TestAdd/正数 (0.00s)
# --- PASS: TestAdd/负数 (0.00s)
Tip表驱动测试是 Go 社区的最佳实践。加新用例只要往表里加一行,不用写新函数。
并行测试
测试互不依赖时可以并行跑,加速:
func TestAdd(t *testing.T) {
t.Parallel() // 标记可并行
tests := []struct{ ... }{...}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // 子测试也并行
got := Add(tt.a, tt.b)
if got != tt.want {
t.Errorf("got %d, want %d", got, tt.want)
}
})
}
}
Benchmark 基准测试
Benchmark 函数以 Benchmark 开头,参数是 *testing.B:
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}
运行:
go test -bench=. # 跑所有 benchmark
go test -bench=BenchmarkAdd # 指定
go test -bench=. -benchmem # 带内存分配统计
输出:
BenchmarkAdd-8 1000000000 0.3 ns/op 0 B/op 0 allocs/op
-8:GOMAXPROCS1000000000:跑了 10 亿次0.3 ns/op:每次操作 0.3 纳秒0 B/op:每次分配 0 字节0 allocs/op:每次 0 次分配
B.Loop(Go 1.24+)
Go 1.24 引入 b.Loop(),比手动 for i < b.N 更准确:
func BenchmarkAdd(b *testing.B) {
for b.Loop() {
Add(1, 2)
}
}
Note
b.Loop会自动处理编译器优化问题(防止循环被优化掉)。Go 1.26 进一步修复了b.Loop阻止内联的问题。新代码推荐用b.Loop代替for i < b.N。
Example 示例测试
以 Example 开头的函数既是文档又是测试:
func ExampleAdd() {
fmt.Println(Add(1, 2))
// Output: 3
}
// Output: 后面写期望输出,不匹配就测试失败。这种测试会出现在 go doc 里当文档用。
模糊测试(Go 1.18+)
模糊测试(Fuzzing)自动生成随机输入,帮你找边界 bug。函数以 Fuzz 开头:
func FuzzAdd(f *testing.F) {
// 种子用例
f.Add(1, 2)
f.Add(-1, 1)
f.Fuzz(func(t *testing.T, a, b int) {
result := Add(a, b)
// 逆向验证
if result-a != b {
t.Errorf("Add(%d, %d) = %d, 不满足 a+b-b=a", a, b, result)
}
})
}
运行:
go test -fuzz=FuzzAdd # 开始模糊测试
go test -fuzz=FuzzAdd -fuzztime=30s # 跑 30 秒
模糊测试会持续生成随机输入,发现崩溃或断言失败就把输入存到 testdata/fuzz/ 里,之后普通 go test 会自动复现。
Tip模糊测试适合解析器、字符串处理、加解密这类「输入空间大」的代码。它能找到你没想到的边界情况。
测试辅助函数
t.Helper() 标记辅助函数,失败时报错指向调用处而非辅助函数内部:
func assertEqual(t *testing.T, got, want int) {
t.Helper() // 标记为辅助
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
func TestSomething(t *testing.T) {
assertEqual(t, Add(1, 2), 3) // 失败时指向这行,不是 assertEqual 内部
}
go vet 静态检查
go vet 检查常见错误:
go vet ./...
能发现:锁复制、printf 格式不对、不可达代码、结构体标签错误等。
go fix 现代化器(Go 1.26 重写)
§04 介绍过 go fix 的详细用法。这里从工程化角度再强调一下:Go 1.26 全面重写了 go fix,变成「现代化器」,一键把旧代码升级为新写法:
go fix ./...
它会做几十种自动修复,比如:
- 用
slices.Sort替代sort.Slice - 用
min/max内置函数替代手写比较 - 用新标准库 API 替代旧写法
- 执行
//go:fix inline指令做 API 迁移
Note
go fix基于go vet同款分析框架,只做不改变行为的升级。跑完建议 review 一下再提交。旧的 fixer 已全部移除。
gofmt 格式化
gofmt -w . # 格式化当前目录所有文件
go fmt ./... # 等价,格式化所有包
Go 社区统一用 gofmt 格式,省去争论代码风格的时间。
常用测试命令汇总
go test ./... # 跑所有测试
go test -v ./... # 详细输出
go test -race ./... # 带竞态检测
go test -cover ./... # 覆盖率
go test -coverprofile=c.out # 生成覆盖率报告
go tool cover -html=c.out # 看覆盖率网页
go test -bench=. # 跑 benchmark
go test -fuzz=FuzzXxx # 模糊测试
go vet ./... # 静态检查
go fix ./... # 现代化升级
小结
- 测试文件
_test.go,函数TestXxx(t *testing.T) - 表驱动测试是 Go 最佳实践,用
t.Run做子测试 - Benchmark 用
for b.Loop()(1.24+),-benchmem看内存 - 模糊测试(1.18+)自动生成输入找边界 bug
go vet静态检查,go fix(1.26 重写)自动现代化代码gofmt统一代码风格- CI 里建议跑
go test -race -cover ./...加go vet
下一节是最后一章,讲运行时性能和学习路线。