Go 测试:testing 包与测试函数
Go 语言内置了强大的测试支持,无需额外安装框架。通过标准库中的 testing 包,你可以快速编写单元测试、基准测试和示例代码。所有测试文件都以 _test.go 结尾,测试函数必须满足特定命名规则,Go 工具链会自动识别并执行它们。
编写第一个测试函数
- 创建一个名为
math.go的文件,内容如下:
package main
func Add(a, b int) int {
return a + b
}
-
在同一目录下创建
math_test.go文件(注意文件名必须以_test.go结尾)。 -
在
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)
}
}
- 在终端中运行
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)
}
})
}
}
- 定义一个匿名结构体切片
tests,每项代表一组测试用例。 - 遍历该切片,对每个用例调用
t.Run()创建子测试。 - 在子测试中执行实际逻辑并验证结果。
使用
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 还支持测量代码性能。
- 创建基准测试函数,以
Benchmark开头,参数为*testing.B:
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
- 运行基准测试:
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
}
- 运行
go test时,Go 会自动比对fmt.Println输出与// Output:注释是否一致。 - 同时,
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() 才会并行。对于有状态的测试,不要启用并行。
组织大型项目的测试结构
对于多模块项目:
- 将测试文件与源文件放在同一目录(Go 官方推荐)。
- 使用子测试分组逻辑相关的用例,如
t.Run("valid input", ...)和t.Run("invalid input", ...)。 - 提取公共辅助函数到
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,确保代码质量和并发安全。

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