Skip to content
Go
testing — Go 测试框架完全指南

testing — Go 测试框架完全指南

Go 的测试是内置在语言工具链中的,遵循约定优于配置的原则。


1. 测试文件规范

文件命名

  • 测试文件必须以 _test.go 结尾。
  • 测试文件与被测试代码放在同一目录

函数签名

测试类型 函数名格式 参数
单元测试 TestXxx t *testing.T
基准测试 BenchmarkXxx b *testing.B
示例测试 ExampleXxx 无(或无参数)
模糊测试 FuzzXxx f *testing.F

包名选择

// 白盒测试:可以访问未导出成员
package mypackage

// 黑盒测试:只能访问导出成员(推荐用于集成测试)
package mypackage_test

2. testing.T 常用方法

2.1 错误报告

// Error — 记录错误,继续执行(适合多个断言)
t.Error("unexpected value")
t.Errorf("got %v, want %v", got, want)

// Fatal — 记录错误,立即终止(适合初始化失败等不可恢复的错误)
t.Fatal("cannot continue")
t.Fatalf("initialization failed: %v", err)

💡 选择指南:多个独立断言 → Error;后续测试依赖此结果 → Fatal

2.2 t.Helper() — 标记辅助函数

func assertEqual[T comparable](t *testing.T, got, want T) {
    t.Helper()  // ← 关键:错误报告的行号指向调用者,不是这里
    if got != want {
        t.Errorf("got %v, want %v", got, want)
    }
}

func TestSomething(t *testing.T) {
    result := Add(2, 3)
    assertEqual(t, result, 5)  // 错误会指向这一行
}

🚨 陷阱:辅助函数的第一行必须是 t.Helper(),否则错误报告中函数位置会误导调试。


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

Go 社区的核心测试模式:

func TestAdd(t *testing.T) {
    tests := []struct {
        name    string
        a, b    int
        want    int
        wantErr bool
    }{
        {"positive numbers", 2, 3, 5, false},
        {"negative numbers", -1, -2, -3, false},
        {"zero", 0, 0, 0, false},
        {"overflow", math.MaxInt, 1, 0, true},
    }

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

💡 优势

  • go test -run TestAdd/positive 单独运行一个子用例。
  • 失败时子测试名清晰指示哪个用例失败。
  • 每个子测试独立,互不影响。

4. 并行测试与清理

func TestParallel(t *testing.T) {
    t.Parallel()  // 声明可与其他 Parallel 测试并发运行

    t.Cleanup(func() {
        os.Remove("temp.db") // 测试结束后自动调用
    })

    tmpDir := t.TempDir() // 自动创建 + 自动删除的临时目录

    t.Setenv("APP_ENV", "test") // 自动恢复环境变量
}

💡 最佳实践

  • 所有需要文件系统的测试都用 t.TempDir()
  • 所有修改环境变量的测试都用 t.Setenv()
  • 测试中创建的外部资源用 t.Cleanup() 注册清理。

5. 基准测试 (Benchmark)

func BenchmarkAdd(b *testing.B) {
    data := prepareData()  // 不计时
    
    b.ResetTimer()         // ← 重置计时器(忽略准备时间)
    
    for i := 0; i < b.N; i++ {
        Add(data[i%len(data)])
    }
    
    b.ReportAllocs()       // 报告内存分配
}

// 运行:
// go test -bench=. -benchmem
// go test -bench=Add -benchtime=5s -count=3

基准测试关键方法

b.ResetTimer()    // 重置计时器
b.StopTimer()     // 暂停计时(用于 b.N 循环外的耗时操作)
b.StartTimer()    // 恢复计时
b.ReportAllocs()  // 报告内存分配统计

6. 示例测试 (Example)

func ExampleAdd() {
    fmt.Println(Add(2, 3))
    // Output: 5
}

func ExampleAdd_negative() {
    fmt.Println(Add(-1, -2))
    // Output: -3
}

三个作用:

  1. go doc 中作为文档展示。
  2. go test 验证输出是否与 // Output: 匹配。
  3. 验证代码可编译运行。

7. 模糊测试 (Fuzzing, Go 1.18+)

模糊测试(Fuzzing)是一种自动化测试技术:向被测试函数注入随机、非预期的输入,检测它是否会崩溃、死循环或产生错误结果。它特别适合解析器、编码器、网络协议实现等需要处理任意输入的代码。

7.1 🔬 种子语料 (Seed Corpus) 是什么?

种子语料是提供给模糊测试引擎的初始输入集合

func FuzzParse(f *testing.F) {
    // 🔬 种子语料:这些是"已知合理的"输入,模糊测试从它们开始
    f.Add("hello")        // 种子 1:普通英文
    f.Add("123")          // 种子 2:纯数字
    f.Add("hello world!") // 种子 3:带空格
    f.Add("")             // 种子 4:空字符串(边界情况)
    f.Add("你好世界")     // 种子 5:Unicode

    f.Fuzz(func(t *testing.T, input string) {
        result, err := Parse(input)
        // 不依赖固定输出,检查属性不变式
        if err == nil && len(result) == 0 && len(input) > 0 {
            t.Errorf("empty result for non-empty input: %q", input)
        }
    })
}

为什么需要种子?

模糊引擎会从种子出发,通过变异(修改、插入、删除字节)生成大量新输入。好的种子能帮助模糊引擎更快地探索代码路径。

种子类型 示例 作用
正常输入 "hello" 覆盖主流程
边界值 """ " 覆盖边界条件
特殊格式 "key=value" 覆盖特定格式解析
历史 Bug 输入 曾导致 crash 的输入 防止回归
极端值 超长字符串 覆盖缓冲区溢出路径

种子从哪里来?

  1. 手动添加:开发时通过 f.Add() 添加。
  2. 语料库目录:Go 会在 testdata/fuzz/FuzzXxx/ 目录下保存模糊测试发现的"有趣"输入。这些文件会成为后续 go test 的种子。
  3. 模糊引擎自动发现:引擎在运行中发现了覆盖新代码路径的输入,会自动保存到语料库。
# 模糊测试会在 testdata/fuzz/ 下生成语料文件
# 这些文件应该提交到 git(作为回归测试的种子)
testdata/fuzz/FuzzParse/
├── a1b2c3d4e5f6    # 模糊测试发现的输入
├── 7f8e9d0c1b2a    # 另一个变异输入
└── ...

💡 最佳实践

  • testdata/fuzz/ 目录提交到版本控制,作为持续的回归测试。
  • 每个 FuzzXxx 的语料自动保存在 testdata/fuzz/FuzzXxx/ 下。
  • 运行 go test 时也会自动执行语料库中的种子(不进行模糊变异)。

7.2 模糊测试的结构

func FuzzParse(f *testing.F) {
    // 阶段 1:注册种子语料
    f.Add("hello")
    f.Add("123")

    // 阶段 2:定义模糊测试函数
    f.Fuzz(func(t *testing.T, input string) {
        // 模糊测试函数不依赖固定输出
        // 而是检查"不变式"(invariant)
        result, err := Parse(input)
        if err == nil && len(result) == 0 && len(input) > 0 {
            t.Errorf("empty result for non-empty input: %q", input)
        }
    })
}

8. 常用运行命令

go test                           # 运行当前包所有测试
go test ./...                     # 递归运行所有测试
go test -v                        # 详细输出
go test -run TestAdd              # 只运行匹配的测试
go test -run TestAdd/positive     # 只运行子测试
go test -count=3                  # 运行 3 遍(检测 flaky test)
go test -race                     # 启用竞态检测
go test -coverprofile=c.out       # 生成覆盖率
go tool cover -html=c.out         # 覆盖率可视化
go test -bench=. -benchmem        # 运行基准测试

9. 测试覆盖率

go test -coverprofile=coverage.out
go tool cover -func=coverage.out   # 按函数统计
go tool cover -html=coverage.out   # 在浏览器中看

💡 最佳实践:覆盖率不是越高越好。追求 100% 覆盖往往意味着测试了 getter/setter 等无意义代码。关注核心逻辑的覆盖率 > 80%。


10. 🚨 测试常见陷阱

陷阱 正确做法
辅助函数不调 t.Helper() 错误指向辅助函数内部的行
循环中 tt := tt 忘记(Go < 1.22) 总是写 tt := tt 或升级到 Go 1.22+
测试之间共享全局状态 每个测试独立初始化
os.Setenv 在测试中 t.Setenv()
手动创建临时文件 t.TempDir()