在 Go 语言中,测试不是“一等公民”,而是“超等公民”。Go 编译器自带了 go test 工具,标准库提供了 testing 包,这使得编写测试变得异常简单且规范。

本章将详细介绍:

  1. 单元测试:如何编写基础测试用例。
  2. 表格驱动测试:Go 社区推荐的最佳实践,如何用更少的代码覆盖更多的场景。
  3. 基准测试 (Benchmark):如何科学地测量代码的性能。

1. 基础单元测试

Go 的测试文件必须以 _test.go 结尾,测试函数必须以 Test 开头。

假设我们要测试一个简单的加法函数(保存为 math.go):

1package math
2
3func Add(a, b int) int {
4    return a + b
5}

对应的测试文件(保存为 math_test.go):

 1package math
 2
 3import (
 4    "testing"
 5)
 6
 7// 测试函数签名:func TestXxx(t *testing.T)
 8func TestAdd(t *testing.T) {
 9    got := Add(1, 2)
10    want := 3
11
12    if got != want {
13        // t.Errorf 输出错误信息,测试继续执行
14        // t.Fatalf 输出错误信息,并立即终止当前测试函数
15        t.Errorf("Add(1, 2) = %d; want %d", got, want)
16    }
17}

运行命令:

1$ go test -v
2### RUN   TestAdd
3--- PASS: TestAdd (0.00s)
4PASS
5ok      example.com/math    0.392s

2. 表格驱动测试 (Table-Driven Tests)

如果要测试多种情况(整数、负数、零),写多个 if 语句会很繁琐。Go 惯用 表格驱动 的方式,将输入和期望输出定义在一个切片中,然后遍历执行。

 1func TestAddTableDriven(t *testing.T) {
 2    // 1. 定义测试用例结构
 3    tests := []struct {
 4        name string // 用例名称
 5        a, b int    // 输入参数
 6        want int    // 期望结果
 7    }{
 8        {"Positive", 1, 2, 3},
 9        {"Negative", -1, -2, -3},
10        {"Mixed", -1, 1, 0},
11        {"Zero", 0, 0, 0},
12    }
13
14    // 2. 遍历执行
15    for _, tt := range tests {
16        // t.Run 启动子测试,方便定位具体的错误用例
17        t.Run(tt.name, func(t *testing.T) {
18            got := Add(tt.a, tt.b)
19            if got != tt.want {
20                t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.want)
21            }
22        })
23    }
24}

为什么推荐表格驱动?

  • 清晰:数据与逻辑分离,新增用例只需加一行数据。
  • 隔离:使用 t.Run,某一个用例失败不会影响其他用例的报告。

3. 基准测试 (Benchmark)

基准测试用于衡量代码的性能(CPU 时间、内存分配)。函数名以 Benchmark 开头,参数为 *testing.B

1// 这是一个基准测试
2// 目标是运行函数 b.N 次,看看平均耗时
3func BenchmarkAdd(b *testing.B) {
4    // b.N 是 Go 框架自动调整的一个足够大的数(例如 1000, 1000000...)
5    // 以确保运行时间足够长,从而获得稳定的平均值
6    for i := 0; i < b.N; i++ {
7        Add(1, 2)
8    }
9}

运行基准测试(需要加 -bench 参数):

1# . 表示匹配所有基准测试
2$ go test -bench=.
3goos: darwin
4goarch: arm64
5pkg: example.com/math
6BenchmarkAdd-8   1000000000   0.315 ns/op
7PASS
8ok      example.com/math    0.548s

结果解读:

  • BenchmarkAdd-8:8 表示使用了 8 个 CPU 核心(GOMAXPROCS)。
  • 1000000000:循环执行了 10 亿次。
  • 0.315 ns/op:平均每次操作耗时 0.315 纳秒(非常快!)。

4. 测试覆盖率

想知道测试代码覆盖了多少业务逻辑?使用 -cover 参数。

1$ go test -cover
2PASS
3coverage: 100.0% of statements
4ok      example.com/math    0.392s

还可以生成可视化的覆盖率报告:

1go test -coverprofile=coverage.out
2go tool cover -html=coverage.out

这会在浏览器中打开一个 HTML 页面,绿色表示已覆盖,红色表示未覆盖,一目了然。

小结

Go 的测试哲学是简单直接。它没有复杂的断言库(如 assert.Equals),而是鼓励使用原生的 if 判断。虽然写起来稍微啰嗦,但换来的是没有任何魔法、极致的清晰度和可维护性。

练习题

  1. 编写一个 Mul(a, b int) int 乘法函数,并为其编写表格驱动测试,覆盖正数、负数和零的场景。
  2. Mul 函数编写基准测试,比较它和加法函数的性能差异。

相关阅读