znlgis 博客

GIS开发与技术分享 — GDAL · GeoServer · PostGIS · QGIS · OpenLayers · Cesium · FreeCAD · NPOI

第12章 - 测试、基准测试与性能分析

Go 将测试作为语言生态的一等公民,内置了 testing 包和 go test 命令,无需引入第三方框架即可编写单元测试、基准测试和示例。本章系统讲解 Go 的测试体系与性能分析工具。

12.1 测试文件约定

Go 测试遵循严格的命名约定:

  • 测试文件以 _test.go 结尾,与被测代码放在同一目录、同一包。
  • 测试函数以 Test 开头,签名为 func TestXxx(t *testing.T)
  • 基准函数以 Benchmark 开头,签名为 func BenchmarkXxx(b *testing.B)
  • 示例函数以 Example 开头。

12.2 编写单元测试

假设有一个待测函数 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)
    expected := 5
    if result != expected {
        t.Errorf("Add(2, 3) = %d; 期望 %d", result, expected)
    }
}

12.2.1 t.Error 与 t.Fatal

  • t.Error/t.Errorf:标记测试失败,但继续执行后续代码。
  • t.Fatal/t.Fatalf:标记测试失败并立即终止当前测试函数。
  • t.Log/t.Logf:记录日志(仅在失败或 -v 模式下显示)。
  • t.Skip:跳过测试。

12.3 运行测试

go test                    # 运行当前包的测试
go test ./...              # 运行所有包的测试
go test -v                 # 详细模式,显示每个测试
go test -run TestAdd       # 只运行匹配的测试(支持正则)
go test -race              # 启用竞态检测
go test -count=1           # 禁用缓存,强制重跑

12.4 表驱动测试

表驱动测试(table-driven test)是 Go 社区推崇的测试模式:将多组输入输出组织成一张表,循环执行,简洁且易扩展:

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

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) { // 子测试
            if got := Add(tt.a, tt.b); got != tt.expected {
                t.Errorf("Add(%d, %d) = %d; 期望 %d",
                    tt.a, tt.b, got, tt.expected)
            }
        })
    }
}

t.Run 创建子测试,每个用例独立报告,可单独运行:go test -run TestAdd/负数

12.5 测试覆盖率

go test -cover                          # 显示覆盖率百分比
go test -coverprofile=coverage.out      # 生成覆盖率数据文件
go tool cover -html=coverage.out        # 在浏览器中可视化查看
go tool cover -func=coverage.out        # 按函数查看覆盖率

覆盖率帮助识别未被测试触及的代码路径,但需注意:高覆盖率不等于高质量测试,关键在于覆盖重要逻辑和边界条件。

12.6 基准测试

基准测试用于测量代码性能。b.N 由测试框架自动调整,以获得稳定的统计结果:

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(2, 3)
    }
}

func BenchmarkStringConcat(b *testing.B) {
    b.ResetTimer() // 重置计时器,排除准备阶段
    for i := 0; i < b.N; i++ {
        var s string
        for j := 0; j < 100; j++ {
            s += "x"
        }
    }
}

运行基准测试:

go test -bench=.                 # 运行所有基准测试
go test -bench=Add               # 运行匹配的
go test -bench=. -benchmem       # 同时报告内存分配
go test -bench=. -benchtime=5s   # 每个基准运行 5 秒

输出示例:

BenchmarkAdd-8    1000000000    0.30 ns/op    0 B/op    0 allocs/op

含义:使用 8 核,执行了 10 亿次,每次操作 0.30 纳秒,每次分配 0 字节内存、0 次分配。

12.7 示例测试

示例函数既是文档又是测试。带有 // Output: 注释的示例会被 go test 验证输出是否一致:

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

示例会自动展示在 pkg.go.dev 的文档页面中,是绝佳的”活文档”。

12.8 测试辅助工具

12.8.1 setup 与 teardown

使用 TestMain 控制测试的全局初始化和清理:

func TestMain(m *testing.M) {
    // setup: 准备数据库、临时文件等
    setup()
    code := m.Run() // 运行所有测试
    // teardown: 清理资源
    teardown()
    os.Exit(code)
}

12.8.2 t.Helper 与 t.Cleanup

func assertEqual(t *testing.T, got, want int) {
    t.Helper() // 标记为辅助函数,错误定位到调用处
    if got != want {
        t.Errorf("got %d, want %d", got, want)
    }
}

func TestSomething(t *testing.T) {
    t.Cleanup(func() {
        // 测试结束后自动清理
    })
}

12.8.3 第三方断言库 testify

虽然标准库已足够,但社区广泛使用 testify 简化断言:

import "github.com/stretchr/testify/assert"

func TestWithTestify(t *testing.T) {
    assert.Equal(t, 5, Add(2, 3))
    assert.NoError(t, err)
    assert.True(t, condition)
}

12.9 性能分析 pprof

Go 内置强大的性能剖析工具 pprof,可分析 CPU、内存、goroutine、阻塞等。

12.9.1 通过测试生成 profile

go test -cpuprofile=cpu.prof -memprofile=mem.prof -bench=.
go tool pprof cpu.prof

进入交互界面后,常用命令:top(耗时排名)、list 函数名(查看具体行)、web(生成调用图,需 Graphviz)。

12.9.2 在服务中开启 pprof

为长期运行的 HTTP 服务引入 net/http/pprof:

import _ "net/http/pprof" // 空白导入,自动注册路由

func main() {
    go http.ListenAndServe("localhost:6060", nil)
    // ... 业务逻辑 ...
}

然后访问 http://localhost:6060/debug/pprof/ 或使用:

go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30 # CPU
go tool pprof http://localhost:6060/debug/pprof/heap               # 内存

12.10 执行追踪 trace

go tool trace 提供更细粒度的运行时追踪,可视化 goroutine 调度、GC、系统调用等:

go test -trace=trace.out -bench=.
go tool trace trace.out

12.11 测试最佳实践

  1. 测试与代码同目录同包,对未导出函数也能测试;如需测试公共 API,可用 包名_test 外部测试包。
  2. 优先表驱动测试,覆盖正常、边界、异常情况。
  3. 测试应独立、可重复,不依赖执行顺序和外部状态。
  4. -race 运行测试,及早发现并发问题。
  5. 基准测试关注 allocs/op,减少内存分配往往是优化的关键。
  6. 先测量后优化:用 pprof 定位真正的性能瓶颈,避免凭直觉过早优化。

12.12 本章小结

本章讲解了 Go 完整的测试体系:单元测试、表驱动测试、子测试、覆盖率统计、基准测试和示例测试,全部由内置的 testing 包和 go test 命令支持。性能分析方面,pprof 和 trace 是定位 CPU、内存瓶颈和并发问题的利器。”测试先行、测量后优化”是 Go 工程文化的重要组成部分。

下一章我们将学习反射与泛型这两项高级特性。