文章目录

Go 测试:testing 包与测试函数

发布于 2026-04-03 12:40:22 · 浏览 6 次 · 评论 0 条

Go 测试:testing 包与测试函数

Go 语言内置了强大的测试支持,无需额外安装框架。通过标准库中的 testing 包,你可以快速编写单元测试、基准测试和示例代码。所有测试文件都以 _test.go 结尾,测试函数必须满足特定命名规则,Go 工具链会自动识别并执行它们。


编写第一个测试函数

  1. 创建一个名为 math.go 的文件,内容如下:
package main

func Add(a, b int) int {
    return a + b
}
  1. 在同一目录下创建 math_test.go 文件(注意文件名必须以 _test.go 结尾)。

  2. math_test.go 中导入 testing 包,并定义测试函数:

package main

import "testing"

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    expected := 5
    if result != expected {
        t.Errorf("Add(2, 3) = %d; want %d", result, expected)
    }
}
  1. 在终端中运行 go test 命令。如果输出包含 PASS,说明测试通过。

关键规则:测试函数必须以 Test 开头(首字母大写),后接被测函数名,参数必须是 t *testing.T,且位于 _test.go 文件中。


使用表驱动测试提高覆盖率

当需要测试多种输入输出组合时,手动编写多个 if 判断会非常繁琐。Go 推荐使用“表驱动测试”(table-driven tests)。

修改 math_test.go 如下:

package main

import "testing"

func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive numbers", 2, 3, 5},
        {"negative and positive", -1, 5, 4},
        {"zero", 0, 0, 0},
        {"large numbers", 1000, 2000, 3000},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected)
            }
        })
    }
}
  1. 定义一个匿名结构体切片 tests,每项代表一组测试用例。
  2. 遍历该切片,对每个用例调用 t.Run() 创建子测试。
  3. 在子测试中执行实际逻辑并验证结果。

使用 t.Run() 的好处是:即使某个子测试失败,其他用例仍会继续执行,且错误信息会带上用例名称,便于定位问题。


处理错误与失败策略

testing 包提供了多种报告方式:

  • t.Error():记录错误但继续执行后续代码。
  • t.Fatal():记录错误并立即终止当前测试函数。
  • t.Errorf()t.Fatalf():支持格式化字符串,类似 fmt.Printf

选择合适的方法

  • 如果后续断言依赖当前结果,使用 t.Fatalf() 避免空指针等异常。
  • 如果多个独立检查可并行进行,使用 t.Errorf() 一次性暴露所有问题。

例如,测试一个除法函数时:

func TestDivide(t *testing.T) {
    result, err := Divide(10, 2)
    if err != nil {
        t.Fatalf("Unexpected error: %v", err)
    }
    if result != 5 {
        t.Errorf("Divide(10, 2) = %f; want 5", result)
    }

    _, err = Divide(10, 0)
    if err == nil {
        t.Error("Expected error when dividing by zero, got none")
    }
}

运行特定测试

默认 go test 会运行当前包下所有测试。你可以通过参数精确控制:

  • 运行单个测试函数go test -run TestAdd
  • 运行匹配正则的测试go test -run "Test.*Number"
  • 显示详细日志go test -v-v 表示 verbose)
  • 跳过某些测试:在测试函数开头加 if testing.Short() { t.Skip("skipping in short mode") },然后用 go test -short 跳过

注意:-run 后的参数是正则表达式,不是完整函数名。例如 -run Add 也能匹配 TestAdd


基准测试(性能测试)

除了功能验证,Go 还支持测量代码性能。

  1. 创建基准测试函数,以 Benchmark 开头,参数为 *testing.B
func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(2, 3)
    }
}
  1. 运行基准测试:go test -bench=.
    . 表示运行所有基准测试)

输出示例:

BenchmarkAdd-8    1000000000    0.25 ns/op

表示在 8 核 CPU 上,每次调用耗时约 0.25 纳秒。

关键点:循环次数 b.N 由 Go 自动调整,确保测试时间足够长以获得稳定结果。你只需把被测代码放在循环体内。


示例测试(Example Tests)

Go 支持通过 Example 函数生成文档和验证输出。

添加如下函数到 math_test.go

func ExampleAdd() {
    fmt.Println(Add(1, 2))
    // Output: 3
}
  1. 运行 go test 时,Go 会自动比对 fmt.Println 输出与 // Output: 注释是否一致。
  2. 同时godoc 工具会将此示例展示在函数文档中。

注意:Example 函数必须位于 _test.go 文件中,且注释 // Output: 必须顶格写(前面无空格)。


测试辅助工具与最佳实践

工具/技巧 用途 使用方式
go test -cover 查看测试覆盖率 显示未覆盖的代码行
go test -coverprofile=coverage.out 生成覆盖率报告文件 后续可用 go tool cover -html=coverage.out 查看可视化报告
t.Cleanup() 注册清理函数 在测试结束时自动执行资源释放
testing.AllocsPerRun() 测量内存分配次数 用于优化性能敏感代码

创建临时文件的正确方式

func TestWithTempFile(t *testing.T) {
    tmpfile, err := os.CreateTemp("", "example-*.txt")
    if err != nil {
        t.Fatal(err)
    }
    defer os.Remove(tmpfile.Name()) // 清理文件
    defer tmpfile.Close()

    // 在此处使用 tmpfile 进行测试
}

避免全局状态污染:每个测试应独立运行,不要依赖或修改全局变量。必要时在测试前重置状态。


常见错误排查

  • 测试未被执行:检查函数名是否以 Test 开头,文件名是否为 _test.go,包名是否与被测代码一致。
  • 导入循环错误:测试文件与源文件属于同一包,不能在测试中导入当前包(如 import "./main")。正确做法是直接调用同包函数。
  • 并发测试干扰:若多个测试修改共享资源(如环境变量、文件系统),使用 t.Parallel() 需格外小心,或避免并行。

强制顺序执行:默认测试是串行的。只有显式调用 t.Parallel() 才会并行。对于有状态的测试,不要启用并行。


组织大型项目的测试结构

对于多模块项目:

  1. 将测试文件与源文件放在同一目录(Go 官方推荐)。
  2. 使用子测试分组逻辑相关的用例,如 t.Run("valid input", ...)t.Run("invalid input", ...)
  3. 提取公共辅助函数testutil 包(注意:该包也需以 _test.go 结尾,或单独作为内部测试包)。

例如,在 internal/testutil/helpers.go 中定义通用断言:

// internal/testutil/helpers.go
package testutil

import "testing"

func AssertEqual(t *testing.T, got, want interface{}) {
    t.Helper() // 标记此函数为测试辅助,错误定位到调用者
    if got != want {
        t.Errorf("got %v, want %v", got, want)
    }
}

在测试中调用:

func TestAdd(t *testing.T) {
    testutil.AssertEqual(t, Add(2, 3), 5)
}

使用 t.Helper() 可让错误堆栈指向实际测试代码,而非辅助函数内部。


运行测试的完整命令清单

  • go test:运行当前目录所有测试
  • go test ./...:递归运行所有子目录的测试
  • go test -race:启用竞态检测(用于发现并发 bug)
  • go test -timeout 30s:设置超时时间(默认 10 分钟)
  • go test -count=3:重复运行测试 3 次(用于检测偶发失败)

自动化集成建议:在 CI/CD 流程中始终加入 go test -race -cover,确保代码质量和并发安全。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文