2. Go 测试基础回顾:testing 包核心用法、Test 函数编写规范、表格驱动测试

在构建压测闭环之前,必须夯实单元测试基础。Go 语言内置的 testing 包提供了轻量、高效的测试框架,无需第三方库即可完成从简单验证到复杂基准测试的全部工作。本章聚焦 testing 包的核心用法、Test 函数的编写规范以及 Go 社区广泛采用的表格驱动测试模式。

2.1 testing 包核心用法

testing 包是 Go 标准库的一部分,通过 go test 命令驱动。其核心类型与函数如下:

组件 用途 关键方法/字段
testing.T 单元测试与性能测试的控制句柄 Error / Errorf、Fatal / Fatalf、Log / Logf、Run
testing.B 基准测试的控制句柄 N、ResetTimer、ReportAllocs、RunParallel
testing.M 测试主程序,用于自定义 TestMain Run
testing.Coverage 代码覆盖率统计 Coverage()
提示: Error 与 Fatal 的区别在于:Error 记录失败后继续执行当前测试用例,Fatal 则立即终止。在表格驱动测试中,推荐使用 Error 或 Errorf 以便收集所有子用例的失败信息。

2.2 Test 函数编写规范

Go 编译器通过约定而非注解来识别测试函数。必须遵守以下规则:

  • 函数签名:func TestXxx(t *testing.T),其中 Xxx 首字母大写。
  • 文件命名:测试文件必须以 _test.go 结尾,例如 math_test.go。
  • 包归属:测试文件可以与被测代码同包(白盒测试),也可以使用 _test 后缀的独立包(黑盒测试)。
  • 导入:必须导入 testing 包。
  • 无返回值:测试函数不返回任何值,通过 t.Error / t.Fatal 报告失败。
常见错误: 测试函数名以 test 小写开头、参数类型写错、文件未以 _test.go 结尾,都会导致 go test 静默跳过该函数。

以下是一个符合规范的简单测试示例:

// math.go
package mathutil

func Add(a, b int) int {
    return a + b
}

// math_test.go
package mathutil

import "testing"

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("Add(2, 3) = %d; want 5", result)
    }
}

2.3 表格驱动测试

表格驱动测试(Table-Driven Tests)是 Go 社区的标准实践,通过定义测试用例切片,用循环统一执行,极大减少重复代码并提升可维护性。

2.3.1 标准结构

func TestAddTableDriven(t *testing.T) {
    tests := []struct {
        name string
        a, b int
        want int
    }{
        {"positive", 2, 3, 5},
        {"negative", -1, -2, -3},
        {"zero", 0, 0, 0},
        {"mixed", -5, 8, 3},
    }

    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 时会输出每个子用例的名称和结果,便于定位失败点。

2.3.2 表格驱动测试的优势

维度 传统逐用例测试 表格驱动测试
代码重复 高,每个用例重复调用与断言 低,循环统一执行
新增用例 复制粘贴整个函数 在切片中追加一行
失败定位 行号模糊 子用例名称 + 行号精确
边界覆盖 容易遗漏 结构化枚举,覆盖清晰
重构友好 需修改多处 修改循环体一次生效

2.4 测试流程与生命周期

下图展示了 go test 执行测试文件时的完整流程:

go test 执行流程与测试生命周期 go test 命令 编译 _test.go 文件 执行 TestMain (若有) 串行执行 TestXxx 并行执行 (t.Parallel) 输出结果 / 覆盖率 TestMain 可控制 setup/teardown 与 m.Run()

2.5 常用断言模式与子测试

Go 标准库不提供 assert 断言函数,社区推荐直接使用 if 语句配合 t.Error / t.Fatal。对于复杂结构体的比较,可以使用 reflect.DeepEqual 或第三方包 github.com/stretchr/testify/assert。

子测试最佳实践:
  • 每个子测试使用 t.Run 包裹,名称应描述测试场景。
  • 子测试可以嵌套,形成测试树。
  • 结合 t.Parallel 可并发执行子测试,缩短总耗时。
func TestAddNested(t *testing.T) {
    t.Run("positive", func(t *testing.T) {
        t.Run("small", func(t *testing.T) {
            if Add(1, 2) != 3 {
                t.Error("1+2 should be 3")
            }
        })
        t.Run("large", func(t *testing.T) {
            if Add(1000, 2000) != 3000 {
                t.Error("1000+2000 should be 3000")
            }
        })
    })
    t.Run("negative", func(t *testing.T) {
        if Add(-1, -2) != -3 {
            t.Error("-1 + -2 should be -3")
        }
    })
}

2.6 测试覆盖率与 go test 常用标志

go test 提供了丰富的命令行标志,用于控制测试执行与输出:

标志 作用 示例
-v 显示详细输出,包括每个测试的名称与结果 go test -v
-run 按正则表达式匹配测试函数名或子测试名 go test -run "TestAdd/positive"
-count 重复执行测试 n 次(用于消除缓存影响) go test -count=1
-cover 启用覆盖率分析 go test -cover
-coverprofile 输出覆盖率数据到文件 go test -coverprofile=coverage.out
-bench 运行匹配的基准测试 go test -bench=.
-benchmem 基准测试时输出内存分配统计 go test -bench=. -benchmem
提示: 在 CI/CD 环境中,建议始终使用 -count=1 禁用测试缓存,确保每次运行都是真实执行。

2.7 本章小结

  • testing 包是 Go 测试的基石,testing.T 控制单元测试,testing.B 控制基准测试。
  • Test 函数必须遵循 func TestXxx(t *testing.T) 的命名与签名规范。
  • 表格驱动测试通过结构化用例切片 + t.Run 子测试,显著提升测试的可维护性与可读性。
  • 掌握 go test 常用标志,能够精准控制测试范围、输出详细程度与覆盖率收集。