1. Go 测试基础概念

Go 的测试系统内置在 go test 命令中,支持三种主要类型:

  • 单元测试(Unit Tests):函数名以 Test 开头
  • 示例测试(Example Tests):函数名以 Example 开头
  • 基准测试(Benchmarks):函数名以 Benchmark 开头
  • 模糊测试(Fuzz Tests):函数名以 Fuzz 开头(Go 1.18+)

所有测试文件必须以 _test.go 结尾,放在与被测代码相同的包中。

2. 单元测试(Unit Tests)

编写规则

func TestXxx(t *testing.T) {
    // 测试逻辑
    if got := Foo(); got != want {
        t.Errorf("Foo() = %v, want %v", got, want)
    }
    // 或者使用 t.Fatalf 直接失败退出
}

常用方法

  • t.Error / t.Errorf:记录错误,继续执行
  • t.Fatal / t.Fatalf:记录错误,立即停止当前测试
  • t.Log / t.Logf:打印日志(只有测试失败或用 -v 时才显示)
  • t.Helper():标记辅助函数,在错误时不显示该函数的行号
  • 子测试(Subtests)与表驱动测试(Go 1.7+)

表驱动测试(推荐方式)

func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        want     int
    }{
        {"正数", 2, 3, 5},
        {"负数", -1, 1, 0},
        {"零", 0, 0, 0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := Add(tt.a, tt.b); got != tt.want {
                t.Errorf("Add(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.want)
            }
        })
    }
}

运行单元测试

go test                         # 运行当前包所有测试
go test ./...                   # 递归运行所有子包测试
go test -v                      # 显示详细输出(包括 t.Log)
go test -run TestAdd            # 只运行名字匹配 TestAdd 的测试
go test -run TestAdd/negative   # 只运行子测试 'negative'
go test -count=1                # 禁用测试缓存(每次都重新运行)
go test -short                  # 跳过标记为长耗时的测试(配合 if testing.Short())

3. 示例测试(Example Tests)

作用

  • 作为文档示例(会出现在 godoc 中)
  • 同时作为可执行测试

编写方式

func ExampleAdd() {
    fmt.Println(Add(2, 3))
    // Output: 5
}
  • 注释 // Output: 后面的内容必须与实际输出完全一致(包括换行)
  • 可以有多个 Output 行,支持正则匹配(用 // Unordered output:)

运行:go test -run Example 或直接查看文档。

4. 基准测试(Benchmarks)

编写基准函数

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {  // b.N 由测试框架自动调整
        Add(2, 3)
    }
}

表驱动基准测试(推荐)

func BenchmarkAdd(b *testing.B) {
    tests := []struct {
        name string
        a, b int
    }{
        {"small", 1, 2},
        {"large", 1000000, 2000000},
    }

    for _, tt := range tests {
        b.Run(tt.name, func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                Add(tt.a, tt.b)
            }
        })
    }
}

常用运行命令

go test -bench=.                # 运行所有基准测试
go test -bench=BenchmarkAdd     # 只运行特定基准
go test -bench=. -benchmem      # 显示内存分配统计(B/op 和 allocs/op)
go test -bench=. -count=10      # 运行 10 次取平均(减少波动)
go test -bench=. -benchtime=5s  # 每个基准至少运行 5 秒(更准确)
go test -bench=. -cpu=1,4,8     # 测试不同 GOMAXPROCS 下的性能
go test -run=^$ -bench=.        # 跳过单元测试,只跑基准(重要!)

输出解释

BenchmarkAdd-8          100000000       10.5 ns/op      0 B/op      0 allocs/op
  • -8:GOMAXPROCS=8
  • 100000000:循环执行了 1 亿次
  • 10.5 ns/op:平均每次操作 10.5 纳秒
  • 0 B/op0 allocs/op:每次操作分配的内存和次数(需加 -benchmem)

高级技巧

  • 重置定时器:b.ResetTimer()(排除初始化时间)
  • 停止/启动定时器:b.StopTimer() / b.StartTimer()
  • 避免编译器优化:使用 result := Add(x, y) 并防止 result 被优化掉(可赋给全局变量或用 runtime.KeepAlive(result)
func BenchmarkFib(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        Fib(10)
    }
}

5. 模糊测试(Fuzz Testing,Go 1.18+)

编写方式

func FuzzReverse(f *testing.F) {
    f.Add("hello")                  // 种子语料
    f.Fuzz(func(t *testing.T, s string) {
        rev := Reverse(s)
        double := Reverse(rev)
        if double != s {
            t.Errorf("Reverse twice: %q != %q", double, s)
        }
    })
}

运行

go test -fuzz=FuzzReverse       # 运行指定模糊测试
go test -fuzz=. -fuzztime=30s   # 运行所有模糊测试 30 秒

6. 测试覆盖率

go test -cover                    # 显示覆盖率百分比
go test -coverprofile=cover.out   # 生成覆盖率文件
go test -covermode=atomic         # 更精确的并发覆盖统计
go tool cover -html=cover.out     # 生成 HTML 可视化报告

7. 常用最佳实践总结

  • 所有测试文件放在被测包同目录,文件名 _test.go
  • 优先使用表驱动测试
  • 基准测试一定要加 -run=^$ 避免运行单元测试干扰
  • 重要性能代码必须加 -benchmem 观察内存分配
  • 使用 -count=5~10 多跑几次减少测量误差
  • 关键路径用基准测试持续监控性能回归
  • 覆盖率目标一般 ≥80%,关键包争取 100%