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 测试从文件识别到子测试执行的完整流程:

Go 测试执行流程 xxx_test.go go test 编译 + 链接 TestXxx(t *testing.T) 表格驱动 + t.Run 子测试 PASS / FAIL 报告 关键规则 • 文件名 _test.go 后缀 • 函数名 Test 开头 + 大写 • 唯一参数 *testing.T • 无返回值

6. 测试组织与运行对比

下表对比了不同测试组织方式在冒烟回归场景中的适用性:

组织方式 用例复用 独立运行 并行支持 冒烟回归推荐度
独立 Test 函数 低(重复代码多) 是 需手动 ★★☆☆☆
表格驱动 + t.Run 高(数据驱动) 是(按子测试名) 内置支持 ★★★★★
嵌套子测试组 高(共享 setup) 是(按路径) 内置支持 ★★★★☆
TestMain 全局控制 中(全局 setup) 受限 需谨慎 ★★★☆☆