Go 测试基础:testing 包与测试函数规范
本章是《Go 冒烟回归》的测试基石。你将系统掌握 Go 标准库 testing 的核心用法、测试函数的编写规范、表格驱动测试模式以及子测试的组织方式。这些能力直接决定后续冒烟测试与回归测试用例的质量和可维护性。
1. testing 包核心能力
Go 语言内置的 testing
| 测试类型 | 函数签名 | 用途 | 常用方法 |
|---|---|---|---|
| 单元测试 | func TestXxx(t *testing.T) |
验证函数或模块的正确性 | t.Error, t.Fatal, t.Log |
| 基准测试 | func BenchmarkXxx(b *testing.B) |
测量代码性能 | b.N, b.ResetTimer |
| 示例测试 | func ExampleXxx() |
生成可运行的文档示例 | // Output: 注释 |
| 模糊测试 | func FuzzXxx(f *testing.F) |
随机输入发现边界缺陷 | f.Add, f.Fuzz |
关键认知:testing.T 提供了
Error/Errorf(记录失败但继续执行)和 Fatal/Fatalf(记录失败并立即终止当前测试)两种失败报告方式。在冒烟测试中,优先使用 Fatal 避免级联错误干扰诊断。
2. Test 函数编写规范
Go 编译器通过文件名和函数名自动识别测试代码,遵循以下硬性规则:
- 文件名:必须以
_test.go结尾,例如calculator_test.go。 - 函数名:必须以
Test开头,后跟首字母大写的标识符,例如TestAdd、TestUserLogin。 - 参数:唯一参数为
*testing.T,无返回值。 - 文件位置:与待测源码放在同一包内,可访问包内私有标识符。
一个标准的测试函数结构如下:
package calculator
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}
常见陷阱:测试函数名不能写成
Testadd(小写 a),编译器会忽略它。同样,Test_Add 虽然能运行但不推荐,Go 官方风格指南建议使用驼峰命名。
3. 表格驱动测试
表格驱动测试(Table-Driven Tests)是 Go 社区最推崇的测试模式。它将测试用例组织为结构体切片,通过循环逐一执行,极大减少重复代码并提升可读性。
标准实现模板:
func TestDivide(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
wantErr bool
}{
{"positive", 10, 2, 5, false},
{"negative", -6, 3, -2, false},
{"divide by zero", 5, 0, 0, true},
{"zero numerator", 0, 7, 0, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Divide(tt.a, tt.b)
if (err != nil) != tt.wantErr {
t.Errorf("Divide(%d, %d) error = %v, wantErr %v", tt.a, tt.b, err, tt.wantErr)
return
}
if got != tt.expected {
t.Errorf("Divide(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.expected)
}
})
}
}
表格驱动测试的核心优势:
- 声明式用例:每个用例一行数据,新增测试只需追加结构体元素。
- 自文档化:
name字段描述测试场景,失败时直接定位。 - 与子测试天然结合:
t.Run让每个用例独立执行、独立报告。 - 边界覆盖:容易枚举正常值、边界值、异常值。
冒烟测试最佳实践:在冒烟回归阶段,表格驱动测试的每个用例应覆盖一个核心功能路径。建议将冒烟用例放在表格的前 3-5 行,并添加
smoke 前缀命名,例如 "smoke_basic_add",便于快速筛选执行。
4. 子测试与 t.Run
子测试(Subtests)是 Go 1.7 引入的特性,通过 t.Run 在测试函数内创建嵌套的测试上下文。每个子测试拥有独立的 *testing.T 实例,可以单独运行、跳过或并行执行。
子测试的核心能力:
| 特性 | 说明 | 示例 |
|---|---|---|
| 独立执行 | 使用 go test -run TestXxx/子测试名 精确运行 |
go test -run TestDivide/positive |
| 嵌套层级 | 子测试可无限嵌套,形成树状结构 | t.Run("group", func(t *testing.T) { t.Run("sub", ...) }) |
| 并行控制 | 子测试内调用 t.Parallel() 实现并发 |
多个子测试共享 t.Parallel() 时并行执行 |
| Setup/Teardown | 外层函数可做初始化,子测试结束后自动清理 | 数据库连接在外层建立,子测试共享 |
嵌套子测试的典型结构:
func TestUserService(t *testing.T) {
// 外层初始化(例如创建数据库连接)
db := setupDB(t)
defer db.Close()
t.Run("create", func(t *testing.T) {
// 测试创建用户
if err := createUser(db, "alice"); err != nil {
t.Fatal(err)
}
})
t.Run("query", func(t *testing.T) {
// 测试查询用户
user, err := queryUser(db, "alice")
if err != nil {
t.Fatal(err)
}
if user.Name != "alice" {
t.Errorf("got %s, want alice", user.Name)
}
})
}
冒烟回归中的子测试策略:将冒烟测试用例组织为顶级子测试组(例如
t.Run("smoke", ...)),回归测试用例放在另一组。CI 流水线中通过 go test -run /smoke 快速验证核心路径,全量回归时移除 -run 过滤。
5. 测试执行流程与结构图
下图展示了 Go 测试从文件识别到子测试执行的完整流程:
6. 测试组织与运行对比
下表对比了不同测试组织方式在冒烟回归场景中的适用性:
| 组织方式 | 用例复用 | 独立运行 | 并行支持 | 冒烟回归推荐度 |
|---|---|---|---|---|
| 独立 Test 函数 | 低(重复代码多) | 是 | 需手动 | ★★☆☆☆ |
| 表格驱动 + t.Run | 高(数据驱动) | 是(按子测试名) | 内置支持 | ★★★★★ |
| 嵌套子测试组 | 高(共享 setup) | 是(按路径) | 内置支持 | ★★★★☆ |
| TestMain 全局控制 | 中(全局 setup) | 受限 | 需谨慎 | ★★★☆☆ |