Go testing 库完整指南
目录
- 概述与基础
- testing.T 单元测试详解
- testing.B 基准测试详解
- testing.F 模糊测试详解
- testing.M TestMain 详解
- 高级测试模式
- go test 命令详解
- 最佳实践
- 完整示例
概述与基础
testing 包简介
Go 语言的 testing 包是标准库的一部分,提供了自动化测试的完整支持。它与 go test 命令紧密配合,自动识别并执行符合命名规范的测试函数。
核心特性:
- 无需外部依赖,开箱即用
- 支持单元测试、基准测试、模糊测试
- 内置测试覆盖率分析
- 支持并行测试执行
- 提供丰富的测试控制 API
Go 测试哲学
Go 的测试设计遵循以下原则:
- 简单:测试即普通 Go 代码,无复杂框架
- 快速:测试执行高效,鼓励频繁运行
- 一致:统一的命名规范和结构
- 实用:专注于验证行为而非实现细节
测试文件命名规范
测试文件必须以 _test.go 结尾,例如:
math.go # 源代码
math_test.go # 测试代码测试文件会被排除在常规编译之外,仅在执行 go test 时编译。
测试函数命名规范
| 测试类型 | 函数签名 | 说明 |
|---|---|---|
| 单元测试 | func TestXxx(*testing.T) | 验证代码功能正确性 |
| 基准测试 | func BenchmarkXxx(*testing.B) | 测量代码性能 |
| 模糊测试 | func FuzzXxx(*testing.F) | 自动生成输入寻找边界情况 |
| 示例测试 | func ExampleXxx() | 文档化并验证代码使用方式 |
| 测试主函数 | func TestMain(*testing.M) | 控制测试执行流程 |
提示
Xxx 部分的首字母必须大写,这是 Go 导出标识符的规则。
基本测试示例
让我们从一个简单的例子开始:
// math.go
package math
func Add(a, b int) int {
return a + b
}
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, ErrDivisionByZero
}
return a / b, nil
}// math_test.go
package math
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
go test -v # 详细输出testing.T 单元测试详解
核心断言方法
| 方法 | 说明 | 何时使用 |
|---|---|---|
Error(args ...any) | 记录错误,继续执行测试 | 验证失败但后续测试仍有意义 |
Errorf(format, ...) | 格式化记录错误 | 需要格式化错误信息 |
Fail() | 标记测试失败但继续 | 需要自定义失败逻辑 |
FailNow() | 标记测试失败并立即停止 | 无法继续测试时使用 |
Fatal(args ...any) | 记录错误并立即停止 | 关键验证失败,继续无意义 |
Fatalf(format, ...) | 格式化记录错误并停止 | 关键验证失败,需要格式化信息 |
Log(args ...any) | 记录日志(仅失败或 -v 时显示) | 输出调试信息 |
Logf(format, ...) | 格式化记录日志 | 格式化调试信息 |
Skip(args ...any) | 跳过当前测试 | 条件不满足时跳过 |
Skipf(format, ...) | 格式化跳过当前测试 | 条件不满足时跳过,带信息 |
SkipNow() | 立即跳过当前测试 | 需要立即跳过时 |
示例:
func TestDivide(t *testing.T) {
// 正常情况
result, err := Divide(10, 2)
if err != nil {
t.Fatalf("Divide(10, 2) unexpected error: %v", err)
}
if result != 5 {
t.Errorf("Divide(10, 2) = %d; want 5", result)
}
// 除以零情况
_, err = Divide(10, 0)
if err != ErrDivisionByZero {
t.Errorf("Divide(10, 0) error = %v; want ErrDivisionByZero", err)
}
}状态方法
| 方法 | 说明 |
|---|---|
Failed() | 返回测试是否已失败 |
Skipped() | 返回测试是否已跳过 |
Name() | 返回当前测试名称 |
辅助方法
Helper()
标记当前函数为测试辅助函数,错误信息会显示调用者位置而非辅助函数内部:
func assertEqual(t *testing.T, expected, actual int) {
t.Helper() // 关键:标记为辅助函数
if expected != actual {
t.Errorf("expected %d, got %d", expected, actual)
}
}
func TestAdd(t *testing.T) {
assertEqual(t, 5, Add(2, 3)) // 错误信息指向这里,而非 assertEqual 内部
}Cleanup()
注册测试完成后执行的清理函数,按注册的逆序执行:
func TestWithTempFile(t *testing.T) {
file, err := os.Create("temp.txt")
if err != nil {
t.Fatal(err)
}
// 注册清理函数
t.Cleanup(func() {
file.Close()
os.Remove("temp.txt")
})
// 使用文件进行测试...
}测试上下文方法
| 方法 | 说明 |
|---|---|
TempDir() | 创建临时目录,测试后自动删除 |
Setenv(key, value) | 设置环境变量,测试后自动恢复 |
Chdir(dir) | 切换工作目录,测试后自动恢复 |
Context() | 返回测试上下文 |
Deadline() | 返回测试截止时间(如果有) |
示例:
func TestWithTempDir(t *testing.T) {
tempDir := t.TempDir()
t.Logf("Temp dir: %s", tempDir)
t.Setenv("TEST_MODE", "true")
// 环境变量会在测试结束后自动恢复
}子测试:t.Run()
使用 t.Run() 创建子测试,可以更好地组织测试用例:
func TestCalculate(t *testing.T) {
t.Run("add", func(t *testing.T) {
if Add(2, 3) != 5 {
t.Error("addition failed")
}
})
t.Run("subtract", func(t *testing.T) {
if Subtract(5, 3) != 2 {
t.Error("subtraction failed")
}
})
}运行特定子测试:
go test -run TestCalculate/add并行测试:t.Parallel()
标记测试可以与其他并行测试同时运行:
func TestParallel1(t *testing.T) {
t.Parallel() // 标记为并行
// 测试逻辑
}
func TestParallel2(t *testing.T) {
t.Parallel() // 标记为并行
// 测试逻辑
}注意
并行测试会共享测试环境,需要注意并发安全。
testing.B 基准测试详解
基准测试基本结构
func BenchmarkAdd(b *testing.B) {
// 初始化代码(不计入时间)
for i := 0; i < b.N; i++ {
Add(2, 3) // 被测试的代码
}
}Go 1.23+ 还支持更简洁的 b.Loop():
func BenchmarkAdd(b *testing.B) {
for b.Loop() {
Add(2, 3)
}
}计时控制方法
| 方法 | 说明 |
|---|---|
ResetTimer() | 重置计时器,排除初始化时间 |
StartTimer() | 开始计时(默认已开始) |
StopTimer() | 停止计时 |
示例:
func BenchmarkBigOperation(b *testing.B) {
// 昂贵的初始化
bigData := make([]int, 1000000)
for i := range bigData {
bigData[i] = i
}
b.ResetTimer() // 重置计时,排除初始化时间
for b.Loop() {
Process(bigData)
}
}内存统计
使用 ReportAllocs() 启用内存分配统计:
func BenchmarkStringConcat(b *testing.B) {
b.ReportAllocs() // 报告内存分配
for b.Loop() {
var s string
for i := 0; i < 100; i++ {
s += "x"
}
}
}并行基准测试
使用 RunParallel() 测试并发性能:
func BenchmarkParallelAdd(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Add(2, 3)
}
})
}自定义指标
使用 ReportMetric() 添加自定义指标:
func BenchmarkWithMetric(b *testing.B) {
var totalBytes int64
for b.Loop() {
data := Process()
totalBytes += int64(len(data))
}
b.ReportMetric(float64(totalBytes)/float64(b.N), "B/op")
}运行基准测试
go test -bench=. # 运行所有基准测试
go test -bench=Add # 运行匹配的基准测试
go test -bench=. -benchmem # 显示内存分配
go test -bench=. -count=5 # 运行 5 次
go test -bench=. -benchtime=2s # 每个基准测试运行 2 秒testing.F 模糊测试详解
模糊测试基础
模糊测试(Fuzzing)自动生成随机输入来寻找程序的边界情况和潜在 bug。
func FuzzParseInt(f *testing.F) {
// 添加种子用例
f.Add("123")
f.Add("-456")
f.Add("0")
// 模糊测试函数
f.Fuzz(func(t *testing.T, s string) {
_, err := strconv.Atoi(s)
// 即使出错也不崩溃就是成功
})
}运行模糊测试
go test -fuzz=FuzzParseInt # 运行模糊测试
go test -fuzz=FuzzParseInt -fuzztime=10s # 运行 10 秒提示
模糊测试找到的失败用例会被保存到 testdata/fuzz/ 目录,下次运行时会自动重新测试。
支持的类型
模糊测试支持以下类型作为参数:
string,[]byteint,int8,int16,int32,int64uint,uint8,uint16,uint32,uint64float32,float64bool
testing.M TestMain 详解
TestMain 的作用
当需要在所有测试执行前后进行全局设置或清理时,使用 TestMain:
func TestMain(m *testing.M) {
// 测试前的设置
fmt.Println("Setting up...")
setupDatabase()
// 运行测试
code := m.Run()
// 测试后的清理
fmt.Println("Cleaning up...")
teardownDatabase()
// 退出
os.Exit(code)
}注意
如果定义了 TestMain,必须调用 m.Run() 否则测试不会执行。
完整示例
var testDB *sql.DB
func TestMain(m *testing.M) {
// 设置
var err error
testDB, err = sql.Open("sqlite3", ":memory:")
if err != nil {
log.Fatalf("Failed to open test DB: %v", err)
}
defer testDB.Close()
// 创建测试表
_, err = testDB.Exec(`CREATE TABLE users (id INT, name TEXT)`)
if err != nil {
log.Fatalf("Failed to create table: %v", err)
}
// 运行测试
code := m.Run()
os.Exit(code)
}高级测试模式
表格驱动测试(Table-Driven Tests)
表格驱动测试是 Go 中最常用的测试模式,使用结构体切片组织测试用例:
func TestCalculate(t *testing.T) {
tests := []struct {
name string
a, b int
op string
expected int
wantErr bool
}{
{"addition", 2, 3, "+", 5, false},
{"subtraction", 5, 3, "-", 2, false},
{"multiplication", 4, 3, "*", 12, false},
{"division", 10, 2, "/", 5, false},
{"division by zero", 10, 0, "/", 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Calculate(tt.a, tt.b, tt.op)
if (err != nil) != tt.wantErr {
t.Errorf("Calculate() error = %v, wantErr %v", err, tt.wantErr)
return
}
if result != tt.expected {
t.Errorf("Calculate(%d, %d, %q) = %d; want %d",
tt.a, tt.b, tt.op, result, tt.expected)
}
})
}
}白盒测试 vs 黑盒测试
白盒测试(同包):
package mypkg // 与被测代码同包
import "testing"
func TestInternalFunc(t *testing.T) {
// 可以访问未导出的函数和变量
result := internalHelper()
// ...
}黑盒测试(独立包):
package mypkg_test // 使用 _test 后缀
import (
"testing"
"mypkg" // 导入被测包
)
func TestPublicAPI(t *testing.T) {
// 只能访问导出的标识符
result := mypkg.PublicFunc()
// ...
}测试跳过
使用 testing.Short() 检测是否启用短测试模式:
func TestLongRunning(t *testing.T) {
if testing.Short() {
t.Skip("Skipping long test in short mode")
}
// 长时间运行的测试
}运行短测试:
go test -shortgo test 命令详解
常用命令参数
| 参数 | 说明 |
|---|---|
-v | 详细输出模式 |
-run pattern | 只运行匹配的测试 |
-bench pattern | 运行匹配的基准测试 |
-benchmem | 显示基准测试的内存分配 |
-timeout duration | 设置测试超时时间 |
-cover | 启用测试覆盖率分析 |
-short | 运行短测试 |
-race | 启用数据竞争检测 |
-parallel n | 设置最大并行测试数 |
-count n | 运行测试 n 次 |
-failfast | 第一个测试失败后停止 |
测试覆盖率
# 基本覆盖率
go test -cover
# 输出覆盖率文件
go test -coverprofile=cover.out
# HTML 可视化报告
go tool cover -html=cover.out
# 函数级覆盖率
go tool cover -func=cover.out数据竞争检测
go test -race提示
-race 会显著增加测试时间,但能发现难以察觉的并发 bug。
包列表模式 vs 本地目录模式
# 本地目录模式(无缓存)
go test
go test -v
# 包列表模式(有缓存)
go test ./...
go test ./pkg/mypkg
go test math最佳实践
测试结构组织
- 每个包一个测试文件 或 每个源文件对应一个测试文件
- 相关测试组织在一起
- 使用表格驱动测试减少重复代码
- 使用子测试组织复杂测试场景
测试命名规范
- 使用描述性名称:
TestCalculate_DivisionByZero而非TestCalc1 - 遵循
Test{Subject}_{Scenario}模式 - 子测试名称应描述测试场景
断言最佳实践
- 使用
Errorf提供清晰的错误信息:t.Errorf("Add(%d, %d) = %d; want %d", a, b, got, want) - 使用
Fatal当后续测试无意义时 - 考虑使用 testify 等断言库(但非必须)
测试覆盖率策略
- 追求高质量的覆盖而非 100% 的数字
- 重点覆盖关键业务逻辑
- 不要为了覆盖而写无效测试
并行测试注意事项
- 仅在无共享状态时使用
t.Parallel() - 考虑使用
t.Setenv()和t.TempDir()隔离测试 - 注意数据库连接等共享资源
Mock 与依赖注入
- 使用接口抽象依赖
- 创建 mock 实现用于测试
- 避免在测试中访问外部服务
完整示例
单元测试完整示例
package calculator
import "testing"
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"positive", 1, 2, 3},
{"negative", -1, -2, -3},
{"zero", 0, 0, 0},
{"mixed", 1, -2, -1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.want {
t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.want)
}
})
}
}基准测试完整示例
package calculator
import "testing"
func BenchmarkAdd(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
Add(1, 2)
}
}
func BenchmarkAddParallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Add(1, 2)
}
})
}TestMain 完整示例
package calculator
import (
"os"
"testing"
)
var testConfig Config
func TestMain(m *testing.M) {
// 设置测试配置
testConfig = Config{
Mode: "test",
}
// 运行测试
code := m.Run()
os.Exit(code)
}模糊测试完整示例
package calculator
import "testing"
func FuzzParseNumber(f *testing.F) {
f.Add("123")
f.Add("-456")
f.Add("0")
f.Add("3.14")
f.Fuzz(func(t *testing.T, s string) {
_, err := ParseNumber(s)
// 只要不崩溃就是成功
_ = err
})
}总结
Go 的 testing 包提供了完整的测试解决方案,包括:
- 单元测试 (
testing.T) - 验证功能正确性 - 基准测试 (
testing.B) - 测量性能 - 模糊测试 (
testing.F) - 自动发现边界情况 - TestMain (
testing.M) - 控制测试流程
配合 go test 命令的强大功能,可以构建高质量的测试套件。记住:测试是投资,不是负担!