golang test
Go 测试基础概念
Go 的测试能力是语言原生内建的,不需要任何第三方框架。核心命令 go test 会自动发现并运行符合约定的测试代码。Go 提供四种测试类型,均通过函数名前缀区分:
- 单元测试(Unit Tests):函数名以
Test开头,签名func TestXxx(t *testing.T) - 示例测试(Example Tests):函数名以
Example开头,无参数,通过// Output:注释校验输出 - 基准测试(Benchmarks):函数名以
Benchmark开头,签名func BenchmarkXxx(b *testing.B) - 模糊测试(Fuzz Tests):函数名以
Fuzz开头,签名func FuzzXxx(f *testing.F)(Go 1.18+)
文件与包约定
所有测试文件必须以 _test.go 结尾,编译时不会进入最终产物。测试文件放在与被测代码相同的目录下,但包名有两种写法,对应两种测试组织方式:
| 写法 | 包名 | 可访问性 | 适用场景 |
|---|---|---|---|
| 内部包测试 | package foo(与被测包同名) | 可访问未导出成员 | 测试内部实现细节 |
| 外部包测试 | package foo_test | 只能访问导出成员 | 测试公开 API,避免循环导入 |
1 | |
1 | |
社区推荐优先使用外部包测试(foo_test),它强制以调用方视角验证 API,且能打破被测包内部产生的导入环。只有需要验证私有实现时才回退到内部包测试。
单元测试(Unit Tests)
编写规则
测试函数签名固定为 func TestXxx(t *testing.T),其中 Xxx 首字母必须大写,后续部分通常是被测对象的名字。
1 | |
常用方法
t *testing.T 提供的方法分几类:
失败控制
t.Error/t.Errorf:记录错误并标记失败,继续执行当前测试t.Fatal/t.Fatalf:记录错误并立即停止当前测试(跳过后续代码)t.Skip/t.Skipf:跳过当前测试并说明原因t.FailNow():标记失败并立即停止(Fatal 的底层实现)
日志
t.Log/t.Logf:打印日志,仅在-v模式或测试失败时显示
测试组织
t.Run(name, func):创建子测试,支持嵌套t.Parallel():声明当前测试可与其他并行测试并发执行t.Cleanup(func):注册清理函数,测试结束后按 LIFO 顺序执行t.Helper():标记辅助函数,错误定位时跳过该函数的调用栈
环境隔离
t.Setenv(key, val):设置环境变量,测试结束自动恢复原值t.TempDir():返回一个测试结束自动删除的临时目录
下面逐个说明几个关键方法。
t.Helper()
当多个测试共用一个断言辅助函数时,t.Helper() 能让错误堆栈指向调用处而非辅助函数内部,定位更直观:
1 | |
t.Cleanup()、t.Setenv()、t.TempDir()
这三个方法替代了传统的 defer + 手动清理,更适配 Go 测试框架的并行与子测试模型:
1 | |
注意:
t.Setenv会修改进程级环境变量,因此不能与t.Parallel()同时使用。
t.Skip()
1 | |
go test -short 会在 CI 或本地快速迭代时跳过标记了长耗时的测试。
表驱动测试(推荐方式)
表驱动测试是 Go 社区最推崇的单元测试范式:把输入与期望输出组织成一张表,循环执行,每行作为一个子测试。好处是新增用例只改数据、不改逻辑,且每个用例独立报告。
1 | |
表驱动测试的并行版本
当用例之间互不影响时,可以让每个子测试并行运行以缩短总时间。这里有一个经典陷阱:循环变量捕获。
1 | |
Go 1.22 修复了
for循环变量每轮共享的问题,tt := tt不再必需;但在团队混合版本时写上仍是无害的防御性写法。
TestMain:全局 setup/teardown
当所有测试需要共享初始化(如连接测试数据库、启动 mock 服务器)时,定义 TestMain:
1 | |
TestMain 在每个测试包中最多定义一个,且必须调用 m.Run(),否则所有测试都不会执行。
运行单元测试
1 | |
-run接收正则表达式,且对每个/分隔的层级独立匹配,例如-run TestAdd/正会匹配子测试名以"正"开头的用例。
示例测试(Example Tests)
作用
示例测试一举两得:
- 作为文档:会出现在
godoc输出中,紧贴对应函数/类型 - 作为可执行测试:保证文档中的代码始终可编译、可运行、输出正确
这避免了"文档与代码脱节"的经典问题——示例代码一旦失效,测试就会失败。
编写方式
1 | |
命名规则
示例函数的命名决定它在 godoc 中挂载到哪个位置:
| 函数名 | 挂载位置 |
|---|---|
ExampleAdd | 函数 Add |
ExampleAdd_order | 函数 Add,场景 _order(后缀仅用于区分多个示例) |
ExampleUser | 类型 User |
ExampleUser_Save | 类型 User 的 Save 方法 |
Example | 整个包 |
Output 匹配规则
// Output:后内容必须与实际标准输出完全一致(含换行),但会忽略前导空白// Unordered output:不关心多行输出的顺序- 没有任何
// Output注释的示例只编译、不运行(用于纯展示)
1 | |
运行:go test -run Example 或在 godoc 中直接查看。
基准测试(Benchmarks)
编写基准函数
基准测试的核心是让被测代码循环执行 b.N 次,框架会自动调整 b.N 的大小,使总运行时间达到设定的阈值:
1 | |
b.N 的工作原理
b.N 并非固定值。框架的策略是:
- 先用较小的
b.N(如 1)试运行 - 测量耗时,估算要达到
-benchtime(默认 1 秒)需要多大b.N - 逐步放大
b.N重新运行,直到实际运行时间满足阈值
因此同一个基准测试每次跑,b.N 可能不同——这是正常现象,目的是让测量误差足够小。
表驱动基准测试(推荐)
用 b.Run 创建子基准,便于对比不同输入规模下的性能:
1 | |
常用运行命令
1 | |
-run=^$用一个匹配空字符串的正则,确保不运行任何单元测试,避免其耗时干扰基准测量。
输出解释
1 | |
| 列 | 含义 |
|---|---|
BenchmarkAdd-8 | 名称 - 后的 8 表示运行时 GOMAXPROCS=8 |
100000000 | 循环执行了 1 亿次(即 b.N) |
10.5 ns/op | 平均每次操作耗时 10.5 纳秒 |
0 B/op | 每次操作分配的字节数(需加 -benchmem) |
0 allocs/op | 每次操作的内存分配次数(需加 -benchmem) |
内存分配统计
-benchmem 是全局开关,也可以在单个基准内用 b.ReportAllocs() 开启:
1 | |
衡量吞吐量时用 b.SetBytes(n),框架会额外输出 MB/s:
1 | |
高级技巧
定时器控制——初始化耗时不应计入测量:
1 | |
b.ResetTimer():重置计时与内存统计,排除之前的准备时间b.StopTimer()/b.StartTimer():成对使用,在循环内临时暂停计时(少用,开销大)
避免编译器优化——编译器可能发现结果未使用而直接删掉被测代码:
1 | |
常见陷阱
- 被优化掉:未消费的结果可能被编译器消除,务必赋给包级变量或用
runtime.KeepAlive - 未排除初始化:忘了
b.ResetTimer(),导致首次构造数据的开销污染结果 - 波动过大:单次测量不可信,务必加
-count=5~10,必要时用-benchtime拉长 - 忘了
-run=^$:单元测试的耗时被算进基准报告
模糊测试(Fuzz Testing,Go 1.18+)
概念
模糊测试让框架自动生成大量随机输入喂给被测函数,目标是发现人工难以预料的边界输入导致的 panic 或断言失败。它区别于普通测试的关键在于:输入空间不再由人手写,而是由引擎基于种子语料变异生成。
- 种子语料:通过
f.Add()显式添加,通常提交到代码库,作为变异起点 - 生成语料:引擎在 fuzzing 过程中产生的语料,默认存于
$GOCACHE/fuzz/,失败用例会落盘到testdata/fuzz/<FuzzName>/
编写方式
1 | |
f.Fuzz 的回调函数签名中,第一个参数固定是 *testing.T,其后是 fuzzing 的目标参数类型(上例为 string)。
运行与复现
1 | |
普通
go test(不带-fuzz)只会用种子语料跑模糊函数,相当于普通测试;只有带-fuzz才会进入随机变异模式。
当引擎发现失败用例,会把它写入 testdata/fuzz/FuzzReverse/<hash>,并在输出中给出复现命令。之后用普通测试即可复现:
1 | |
这个失败用例文件应提交到代码库,作为回归用例长期保留。
测试覆盖率
1 | |
覆盖率模式(-covermode)
| 模式 | 说明 |
|---|---|
set(默认) | 仅记录每条语句是否被执行 |
count | 记录每条语句被执行的次数 |
atomic | count 的并发安全版本,开启 -race 时必须用 |
跨包统计覆盖率用 -coverpkg:
1 | |
竞态检测器
Go 内建数据竞争检测器,能在运行时发现对共享变量的并发访问冲突:
1 | |
它基于 ThreadSanitizer,会在测试运行时插桩,一旦发现两个 goroutine 无同步地访问同一变量且至少一个是写操作,就报告竞争。代价是运行时间增加数倍、内存占用上升,因此建议:
- 本地开发时按需开启
- CI 中默认开启,作为常规质量门禁
构建产物也可带 -race:go build -race,用于线上灰度排查偶发并发问题(但生产常驻会有性能损耗)。
Mock 与依赖注入
Go 社区普遍不依赖 mock 框架,而是通过接口 + 依赖注入让代码可测:被测代码依赖接口,测试时传入一个手写或生成的假实现(fake/mock)。
1 | |
1 | |
当接口方法多、调用复杂时,可借助代码生成工具减少样板:
- gomock:官方维护,基于 mockgen 生成,支持严格调用顺序校验
- moq:根据接口定义生成结构体 mock
- testify/mock:手动编排,运行时断言调用
第三方测试库 testify
testify 是 Go 生态最流行的断言库,提供比标准库 t.Errorf 更丰富的断言与失败处理。核心子包:
assert:失败后继续执行require:失败立即停止(等价t.Fatal)mock:接口 mocksuite:测试套件(setup/teardown 钩子)
1 | |
testify 是"语法糖"而非必需。团队是否引入取决于偏好:它能让断言更简洁,但标准库
t.Errorf配合表驱动测试已能覆盖绝大多数场景。
常用最佳实践总结
- 测试文件放在被测包同目录,命名
_test.go;优先用foo_test外部包测试 - 单元测试优先采用表驱动结构,新增用例只改数据
- 耗时用例用
t.Parallel()并行,注意循环变量捕获 - 用
t.Cleanup/t.Setenv/t.TempDir替代裸defer,更好适配子测试 - 基准测试务必加
-run=^$避免单元测试干扰 - 重要性能代码加
-benchmem观察内存分配,用b.ReportAllocs()精确定位 - 用
-count=5~10多跑几次降低测量误差,关键路径纳入 CI 防止性能回归 - 模糊测试用于解析、编解码、序列化等输入边界敏感的场景,失败用例落盘长期保留
- CI 中默认开启
-race检测数据竞争 - 覆盖率目标一般 ≥80%,关键包争取更高,但不要为追指标而写无意义测试


