testing — Go 测试框架完全指南
Go 的测试是内置在语言工具链中的,遵循约定优于配置的原则。
1. 测试文件规范
文件命名
- 测试文件必须以
_test.go结尾。 - 测试文件与被测试代码放在同一目录。
函数签名
| 测试类型 | 函数名格式 | 参数 |
|---|---|---|
| 单元测试 | TestXxx |
t *testing.T |
| 基准测试 | BenchmarkXxx |
b *testing.B |
| 示例测试 | ExampleXxx |
无(或无参数) |
| 模糊测试 | FuzzXxx |
f *testing.F |
包名选择
// 白盒测试:可以访问未导出成员
package mypackage
// 黑盒测试:只能访问导出成员(推荐用于集成测试)
package mypackage_test2. testing.T 常用方法
2.1 错误报告
// Error — 记录错误,继续执行(适合多个断言)
t.Error("unexpected value")
t.Errorf("got %v, want %v", got, want)
// Fatal — 记录错误,立即终止(适合初始化失败等不可恢复的错误)
t.Fatal("cannot continue")
t.Fatalf("initialization failed: %v", err)💡 选择指南:多个独立断言 →
Error;后续测试依赖此结果 →Fatal。
2.2 t.Helper() — 标记辅助函数
func assertEqual[T comparable](t *testing.T, got, want T) {
t.Helper() // ← 关键:错误报告的行号指向调用者,不是这里
if got != want {
t.Errorf("got %v, want %v", got, want)
}
}
func TestSomething(t *testing.T) {
result := Add(2, 3)
assertEqual(t, result, 5) // 错误会指向这一行
}🚨 陷阱:辅助函数的第一行必须是
t.Helper(),否则错误报告中函数位置会误导调试。
3. 表驱动测试(Table-Driven Tests)
Go 社区的核心测试模式:
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
want int
wantErr bool
}{
{"positive numbers", 2, 3, 5, false},
{"negative numbers", -1, -2, -3, false},
{"zero", 0, 0, 0, false},
{"overflow", math.MaxInt, 1, 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Add(tt.a, tt.b)
if (err != nil) != tt.wantErr {
t.Errorf("error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("got %d, want %d", got, tt.want)
}
})
}
}💡 优势:
go test -run TestAdd/positive单独运行一个子用例。- 失败时子测试名清晰指示哪个用例失败。
- 每个子测试独立,互不影响。
4. 并行测试与清理
func TestParallel(t *testing.T) {
t.Parallel() // 声明可与其他 Parallel 测试并发运行
t.Cleanup(func() {
os.Remove("temp.db") // 测试结束后自动调用
})
tmpDir := t.TempDir() // 自动创建 + 自动删除的临时目录
t.Setenv("APP_ENV", "test") // 自动恢复环境变量
}💡 最佳实践:
- 所有需要文件系统的测试都用
t.TempDir()。- 所有修改环境变量的测试都用
t.Setenv()。- 测试中创建的外部资源用
t.Cleanup()注册清理。
5. 基准测试 (Benchmark)
func BenchmarkAdd(b *testing.B) {
data := prepareData() // 不计时
b.ResetTimer() // ← 重置计时器(忽略准备时间)
for i := 0; i < b.N; i++ {
Add(data[i%len(data)])
}
b.ReportAllocs() // 报告内存分配
}
// 运行:
// go test -bench=. -benchmem
// go test -bench=Add -benchtime=5s -count=3基准测试关键方法
b.ResetTimer() // 重置计时器
b.StopTimer() // 暂停计时(用于 b.N 循环外的耗时操作)
b.StartTimer() // 恢复计时
b.ReportAllocs() // 报告内存分配统计6. 示例测试 (Example)
func ExampleAdd() {
fmt.Println(Add(2, 3))
// Output: 5
}
func ExampleAdd_negative() {
fmt.Println(Add(-1, -2))
// Output: -3
}三个作用:
go doc中作为文档展示。go test验证输出是否与// Output:匹配。- 验证代码可编译运行。
7. 模糊测试 (Fuzzing, Go 1.18+)
模糊测试(Fuzzing)是一种自动化测试技术:向被测试函数注入随机、非预期的输入,检测它是否会崩溃、死循环或产生错误结果。它特别适合解析器、编码器、网络协议实现等需要处理任意输入的代码。
7.1 🔬 种子语料 (Seed Corpus) 是什么?
种子语料是提供给模糊测试引擎的初始输入集合。
func FuzzParse(f *testing.F) {
// 🔬 种子语料:这些是"已知合理的"输入,模糊测试从它们开始
f.Add("hello") // 种子 1:普通英文
f.Add("123") // 种子 2:纯数字
f.Add("hello world!") // 种子 3:带空格
f.Add("") // 种子 4:空字符串(边界情况)
f.Add("你好世界") // 种子 5:Unicode
f.Fuzz(func(t *testing.T, input string) {
result, err := Parse(input)
// 不依赖固定输出,检查属性不变式
if err == nil && len(result) == 0 && len(input) > 0 {
t.Errorf("empty result for non-empty input: %q", input)
}
})
}为什么需要种子?
模糊引擎会从种子出发,通过变异(修改、插入、删除字节)生成大量新输入。好的种子能帮助模糊引擎更快地探索代码路径。
| 种子类型 | 示例 | 作用 |
|---|---|---|
| 正常输入 | "hello" |
覆盖主流程 |
| 边界值 | ""、" " |
覆盖边界条件 |
| 特殊格式 | "key=value" |
覆盖特定格式解析 |
| 历史 Bug 输入 | 曾导致 crash 的输入 | 防止回归 |
| 极端值 | 超长字符串 | 覆盖缓冲区溢出路径 |
种子从哪里来?
- 手动添加:开发时通过
f.Add()添加。 - 语料库目录:Go 会在
testdata/fuzz/FuzzXxx/目录下保存模糊测试发现的"有趣"输入。这些文件会成为后续go test的种子。 - 模糊引擎自动发现:引擎在运行中发现了覆盖新代码路径的输入,会自动保存到语料库。
# 模糊测试会在 testdata/fuzz/ 下生成语料文件
# 这些文件应该提交到 git(作为回归测试的种子)
testdata/fuzz/FuzzParse/
├── a1b2c3d4e5f6 # 模糊测试发现的输入
├── 7f8e9d0c1b2a # 另一个变异输入
└── ...💡 最佳实践:
- 将
testdata/fuzz/目录提交到版本控制,作为持续的回归测试。- 每个
FuzzXxx的语料自动保存在testdata/fuzz/FuzzXxx/下。- 运行
go test时也会自动执行语料库中的种子(不进行模糊变异)。
7.2 模糊测试的结构
func FuzzParse(f *testing.F) {
// 阶段 1:注册种子语料
f.Add("hello")
f.Add("123")
// 阶段 2:定义模糊测试函数
f.Fuzz(func(t *testing.T, input string) {
// 模糊测试函数不依赖固定输出
// 而是检查"不变式"(invariant)
result, err := Parse(input)
if err == nil && len(result) == 0 && len(input) > 0 {
t.Errorf("empty result for non-empty input: %q", input)
}
})
}8. 常用运行命令
go test # 运行当前包所有测试
go test ./... # 递归运行所有测试
go test -v # 详细输出
go test -run TestAdd # 只运行匹配的测试
go test -run TestAdd/positive # 只运行子测试
go test -count=3 # 运行 3 遍(检测 flaky test)
go test -race # 启用竞态检测
go test -coverprofile=c.out # 生成覆盖率
go tool cover -html=c.out # 覆盖率可视化
go test -bench=. -benchmem # 运行基准测试9. 测试覆盖率
go test -coverprofile=coverage.out
go tool cover -func=coverage.out # 按函数统计
go tool cover -html=coverage.out # 在浏览器中看💡 最佳实践:覆盖率不是越高越好。追求 100% 覆盖往往意味着测试了 getter/setter 等无意义代码。关注核心逻辑的覆盖率 > 80%。
10. 🚨 测试常见陷阱
| 陷阱 | 正确做法 |
|---|---|
辅助函数不调 t.Helper() |
错误指向辅助函数内部的行 |
循环中 tt := tt 忘记(Go < 1.22) |
总是写 tt := tt 或升级到 Go 1.22+ |
| 测试之间共享全局状态 | 每个测试独立初始化 |
os.Setenv 在测试中 |
用 t.Setenv() |
| 手动创建临时文件 | 用 t.TempDir() |