16.4 代码测试
在许多语言中,测试框架由社区提供,不同的项目可能使用不同的框架、断言库与运行器,运行一个陌生项目的测试之前,往往要先弄清它用的是哪一套。Go 把测试放进了工具链:go test 是 go
命令的子命令,testing 是标准库的一个包,二者之间只靠几条命名约定衔接。本节先说明 go test
如何依据这些约定把测试组织成一个可执行程序,再讨论 testing 包的设计取向:报告原语为何如此朴素、子测试如何同时解决参数化与并行,以及模糊测试如何把覆盖率引导的输入生成纳入同一套约定。最后概述近几个版本对 testing 包的扩充。
16.4.1 从约定到可执行程序
go test 依据文件名与函数签名发现测试:以 _test.go 结尾的文件只在测试时编译,其中形如
func TestXxx(*testing.T) 的函数是测试,func BenchmarkXxx(*testing.B) 是基准测试,func FuzzXxx(*testing.F)
是模糊测试,func ExampleXxx() 是示例;示例函数末尾若有以 Output:
开头的注释,运行时会把函数的标准输出与之比较 [Go Authors, n.d.a]。这里没有配置文件,也没有注解 [Go Authors, n.d.d]。下面是一个按字节反转字符串的函数及其测试,本节的例子都以它为对象;按字节反转的 Reverse
也是官方模糊测试教程所用的例子 [Go Authors, n.d.e]:
| |
| |
约定如何变成可以运行的程序?go test 为被测包生成一个 main 包,把找到的测试函数登记在表中,然后把它与被测包、测试文件一起链接成测试二进制并运行。生成代码的模板在 cmd/go/internal/load/test.go 中
[Go Authors, 2026a];以 Go 1.26.10 运行 go test -work,可以在工作目录中看到为上面的包生成的文件,这里删去了模板留下的空行:
| |
测试包若定义了 func TestMain(m *testing.M),生成的 main 会改为调用它,由它在 m.Run()
前后完成全局的准备与清理 [Go Authors, n.d.a]。go test
在构建测试二进制时还会对被测包运行 go vet 中可信度较高的一组检查,发现问题便不再运行测试;在给出包列表的模式下,它会缓存成功的测试结果,测试二进制、命令行以及测试读取的文件与环境变量都未改变时,直接输出上次的结果并标注 (cached)。这两项行为都始于 Go 1.10 [Go Authors, 2018;
Go Authors, 2026b]。
16.4.2 朴素的报告原语
testing 包没有断言函数,只提供几个报告原语:t.Error 与 t.Errorf 记录失败,测试继续执行;t.Fatal
与 t.Fatalf 记录失败后调用 FailNow,后者通过 runtime.Goexit 结束当前 goroutine,运行它的全部延迟调用,然后转入下一个测试。正因为如此,FailNow 必须在运行测试函数的 goroutine 中调用,而不能在测试创建的其他 goroutine 中调用,它也不会停止那些 goroutine
[Go Authors, 2026c]。
不提供断言是有意的选择。Go FAQ 的理由是:好的错误处理意味着一个检查失败之后其他检查继续运行,调试者由此看到完整的失败情形,例如 isPrime 对 2、3、5、7 都给出错误的答案,而不是只知道它在 2 上出错;测试框架又往往发展成自带条件、控制与打印机制的小语言,而 Go 本身已经具备这些能力
[Go Authors, n.d.f]。Go 的测试评审意见更直接地建议避免使用断言库:断言要么提前中止测试,要么丢掉了测试做对了什么的信息,还迫使断言包另造一套子语言,而不是复用 Go 本身
[Go Authors, n.d.c]。同一份文档建议失败消息先给出实际值再给出期望值,通常的格式是
YourFunc(%v) = %v, want %v;got 与 want 这对名字由此成为 Go 测试的惯用语。上面 TestReverse
中的失败消息就是这种格式。代价是样板代码比一行断言多,testify 这样的第三方断言库因此也有大量用户。
辅助函数带来一个具体的问题:辅助函数中调用 t.Error 时,失败报告的行号指向辅助函数内部,而不是调用它的测试,这对定位失败几乎没有帮助。Go 1.9 加入的 t.Helper 把调用者标记为辅助函数,报告位置时跳过这些帧,给出第一个非辅助函数中的位置 [Spare, 2016]。
16.4.3 表驱动测试与子测试
当失败消息需要逐个手写时,把输入与期望写成一张表、用循环逐行检查,就能把写好一条消息的成本摊到所有用例上,Go
FAQ 正是这样建议的 [Go Authors, n.d.f]。这种写法在 Go 社区被称为表驱动测试
[Cheney, 2019]。早期的表驱动测试有两个不便:表中的一行无法单独运行,失败消息也必须自己带上用例的名字。van Lohuizen 在 2015 年提出的子测试解决了这些问题,所列的动机包括从命令行选择表中的单个用例、在子测试中使用 Fail 一类方法、比 TestMain
更细粒度地控制准备与清理的范围,以及更好地控制并行 [van Lohuizen, 2015]。子测试随 Go 1.7 发布 [Go Authors, 2016]。t.Run 在一个新的 goroutine
中运行子测试,直到它返回或调用 t.Parallel 才返回;子测试的名字以斜杠接在父测试之后,-run
的模式也按斜杠分段匹配 [Go Authors, 2026c]。以 Go 1.26.10 运行,删去了耗时:
| |
输出展示了并行子测试的调度方式:每个子测试调用 t.Parallel 后暂停(PAUSE),t.Run 随即返回,循环继续创建下一个子测试;父测试的函数体执行完毕后,暂停的子测试才一起继续(CONT)。父测试直到全部子测试结束才算完成,一组并行子测试因此只与彼此并行
[Go Authors, n.d.a]。若只想运行其中一行,用 go test -run TestReverse/ascii 即可。
这种调度方式曾与 for 循环的语义一起构成一个常见的陷阱。Go 1.22 之前,for 语句声明的变量只创建一次,每次迭代更新它的值 [Go Authors, 2024];并行子测试推迟到循环结束之后才执行,闭包捕获的 tt 已是最后一行,所有子测试实际上都在检查同一个用例,而且照样通过。当时的惯用写法是在闭包之前加一行 tt := tt。Go 1.22 改为每次迭代创建新的变量,这一变化按文件的语言版本生效:由 go.mod 的 go 指令或构建约束确定的版本不低于 Go 1.22 时,才采用新语义
[Go Authors, 2026d]。上面的模块声明的是 go 1.26,因此不需要那一行。
子测试之外,*testing.T 还陆续获得了管理测试资源的方法:t.Cleanup
注册在测试及其子测试结束后运行的清理函数(Go 1.14),t.TempDir 返回测试结束后自动删除的临时目录(Go
1.15),t.Setenv 在测试期间设置环境变量(Go 1.17),t.Context 返回一个在清理函数运行之前取消的上下文,t.Chdir
在测试期间切换工作目录(Go 1.24)[Go Authors, 2026c;
Go Authors, 2025a]。它们的共同点是把资源的生命周期绑定到测试上。修改进程级状态的
t.Setenv 与 t.Chdir 不能与 t.Parallel 同时使用 [Go Authors, 2026c]。
16.4.4 模糊测试
表驱动测试只能检查作者想到的输入。上面的 TestReverse 全部通过,但三行用例都是 ASCII 字符串,按字节反转对它们恰好正确。模糊测试(fuzzing)换一种思路:作者只写出对所有输入都应成立的性质,由工具生成大量输入去寻找反例。FuzzReverse 检查的性质是:合法的 UTF-8 字符串反转之后仍应是合法的 UTF-8。不带 -fuzz 时,go test 只把 f.Add 提供的种子与 testdata/fuzz/FuzzReverse
目录下已有的语料当作普通用例运行一遍;带上 -fuzz 才进入模糊模式
[Go Authors, n.d.b]。以 Go 1.26.10 运行一次:
| |
不到一秒、六百多次执行之后,引擎找到了反例:Δ 在 UTF-8 中占两个字节,按字节反转把它拆开了。每次运行找到的具体输入会不同。引擎先以种子语料测得基准覆盖率,然后在多个工作进程中变异输入,保留能扩大覆盖率的那些(输出中的 new interesting)。这些生成的语料保存在 $GOCACHE/fuzz 中;导致失败的输入则写入 testdata/fuzz/FuzzReverse,随代码一起提交后,此后每一次普通的 go test
都会运行它,这个缺陷由此成为一个永久的回归用例 [Go Authors, n.d.b;
Go Authors, 2022]。覆盖率引导依赖编译器插桩,文档建议在支持插桩的平台上运行模糊测试,目前是 amd64 与 arm64 [Go Authors, n.d.b]。
Go 的模糊测试汇合了两条源流。一条是覆盖率引导的模糊测试:Hockman 的设计草案以 AFL 与 libFuzzer 为参照,并指出 Go 社区已有的 go-fuzz 等工具成功地发现了大量缺陷,但缺少与工具链的原生集成
[Hockman, 2020]。另一条是基于性质的随机测试,源头是 Claessen 与 Hughes 的
QuickCheck:不写具体用例,而写应当恒成立的性质,由工具随机生成输入去证伪
[Claessen and Hughes, 2000]。标准库中的 testing/quick 属于这一脉,它的文档写明该包已经冻结,不再接受新功能 [Go Authors, 2026e]。FuzzReverse
的写法兼有二者:断言的是一条性质,输入由覆盖率反馈驱动生成。草案定稿为提案 #44551,模糊测试随 Go 1.18
发布 [Go Authors, 2022]。
16.4.5 基准测试与并发测试
基准测试使用同一套约定,函数形如 func BenchmarkXxx(b *testing.B)。Go 1.24 引入的 b.Loop 取代了传统的
b.N 循环:每次 -count 中基准函数只执行一次,代价较高的准备与清理也只执行一次;函数调用的参数与结果被保持存活,编译器无法把循环体整个优化掉
[Go Authors, 2025a]。b.N 如何收敛、b.Loop 如何实现以及内存度量等内容,见
16.5。
并发代码的测试有两个专门的工具。以 -race 运行测试,竞态检测器会报告测试执行到的数据竞争(16.2)。Go 1.25 正式发布的 testing/synctest 在一个隔离的「气泡」中运行测试,气泡内的时间是虚拟的,所有 goroutine 都持久阻塞时时钟立即前进,依赖超时与定时器的测试因此既快又确定
[Go Authors, 2025b];它对气泡内死锁的检测见 16.1.4。
16.4.6 近年的扩充
最近几个版本为 testing 包增加的接口,大多服务于测试与外部系统的衔接
[Go Authors, 2025a; Go Authors, 2025b;
Go Authors, 2026f]:
| 版本 | 新增 | 作用 |
|---|---|---|
| Go 1.24 | T.Context、T.Chdir、B.Loop | 绑定到测试生命周期的上下文与工作目录;新的基准循环 |
| Go 1.25 | T.Attr、T.Output、testing/synctest | 向测试日志输出键值属性,其含义交给持续集成系统与测试框架;获取与 t.Log 同一输出流的 io.Writer;并发测试的虚拟时间 |
| Go 1.26 | T.ArtifactDir、testing/cryptotest.SetGlobalRandom | 保存测试产物的目录;为密码学函数设定确定的随机源 |
ArtifactDir 返回的目录在给出 go test -artifacts 时位于输出目录之下,可供事后检查,否则是测试结束后删除的临时目录 [Go Authors, 2026f]。SetGlobalRandom 的出现与
Go 1.26 的另一项变化有关:crypto 各包生成密钥等函数不再使用调用者传入的随机源,而总是使用安全的随机字节,需要确定性的测试由此改用这个函数 [Go Authors, 2026f]。表中各接口的引入版本与 api/go1.24.txt 至 api/go1.26.txt 的记录一致。
16.4.7 小结
Go 的测试建立在几条命名约定之上:go test 依据约定发现测试,生成一个 main 包把它们组织成测试二进制。testing
包刻意保持朴素,只提供报告原语,把表达失败的责任留给测试作者,并以表驱动测试摊薄这一成本;子测试让表中的每一行可以单独运行、并行执行;模糊测试把覆盖率引导的输入生成纳入同一套约定,找到的反例以语料文件的形式成为回归用例。
这一设计也有放弃的东西。断言、模拟对象等便利留给了第三方库或样板代码,测试的发现规则也无法重新定义;换来的是所有 Go 项目以同样的方式组织与运行测试,持续集成、覆盖率工具与编辑器都只需面对一套约定。这与 Go 在其他方面的取舍一致(1.1):以一套人人理解、工具都能识别的简单约定,换取整个生态协作上的低摩擦。
延伸阅读
- [Cheney, 2019] Dave Cheney. Prefer table driven tests. 2019. https://dave.cheney.net/2019/05/07/prefer-table-driven-tests
- [Claessen and Hughes, 2000] Koen Claessen and John Hughes. QuickCheck: A Lightweight Tool for Random Testing of Haskell Programs. In Proceedings of the Fifth ACM SIGPLAN International Conference on Functional Programming (ICFP ‘00), 2000. https://doi.org/10.1145/351240.351266 基于性质的随机测试的源头。
- [Go Authors, n.d.a] The Go Authors. Package testing.
https://pkg.go.dev/testing
T、B、F的完整 API 与文档。 - [Go Authors, n.d.b] The Go Authors. Go Fuzzing. https://go.dev/doc/security/fuzz/ 原生模糊测试的用法、语料目录约定与平台要求。
- [Go Authors, n.d.c] The Go Authors. Go Test Comments(wiki).
https://go.dev/wiki/TestComments
YourFunc(%v) = %v, want %v的失败消息格式与避免使用断言库的理由。 - [Go Authors, n.d.d] The Go Authors. Tutorial: Add a test. https://go.dev/doc/tutorial/add-a-test
- [Go Authors, n.d.e] The Go Authors. Tutorial: Getting started with
fuzzing. https://go.dev/doc/tutorial/fuzz
以
Reverse为例的模糊测试教程。 - [Go Authors, n.d.f] The Go Authors. Frequently Asked Questions (FAQ): Where is my favorite helper function for testing? https://go.dev/doc/faq#testing_framework
- [Go Authors, 2016] The Go Authors. Go 1.7 Release Notes. 2016. https://go.dev/doc/go1.7 子测试与子基准测试。
- [Go Authors, 2018] The Go Authors. Go 1.10 Release Notes. 2018.
https://go.dev/doc/go1.10
测试结果缓存;
go test自动运行go vet。 - [Go Authors, 2022] The Go Authors. Go 1.18 Release Notes. 2022.
https://go.dev/doc/go1.18#fuzzing
原生模糊测试;生成的语料写入
$GOCACHE/fuzz。 - [Go Authors, 2024] The Go Authors. Go 1.22 Release Notes. 2024.
https://go.dev/doc/go1.22
for循环的每次迭代创建新的变量。 - [Go Authors, 2025a] The Go Authors. Go 1.24 Release Notes. 2025.
https://go.dev/doc/go1.24
T.Context、T.Chdir、B.Loop。 - [Go Authors, 2025b] The Go Authors. Go 1.25 Release Notes. 2025.
https://go.dev/doc/go1.25
T.Attr、T.Output;testing/synctest转为正式功能。 - [Go Authors, 2026a] The Go Authors. src/cmd/go/internal/load/test.go:
testmainTmpl. Go 1.26 源码. https://github.com/golang/go/blob/release-branch.go1.26/src/cmd/go/internal/load/test.go - [Go Authors, 2026b] The Go Authors. src/cmd/go/internal/test/test.go.
Go 1.26 源码. https://github.com/golang/go/blob/release-branch.go1.26/src/cmd/go/internal/test/test.go
go test自动运行的go vet检查子集。 - [Go Authors, 2026c] The Go Authors. src/testing/testing.go. Go 1.26 源码. https://github.com/golang/go/blob/release-branch.go1.26/src/testing/testing.go
- [Go Authors, 2026d] The Go Authors.
src/cmd/compile/internal/noder/writer.go. Go 1.26 源码.
https://github.com/golang/go/blob/release-branch.go1.26/src/cmd/compile/internal/noder/writer.go
按文件的语言版本决定
for循环变量的语义。 - [Go Authors, 2026e] The Go Authors. src/testing/quick/quick.go. Go 1.26 源码. https://github.com/golang/go/blob/release-branch.go1.26/src/testing/quick/quick.go
- [Go Authors, 2026f] The Go Authors. Go 1.26 Release Notes. 2026.
https://go.dev/doc/go1.26
T.ArtifactDir与go test -artifacts;testing/cryptotest.SetGlobalRandom。 - [Hockman, 2020] Katie Hockman. Design Draft: First Class Fuzzing. Go design draft, 2020. https://go.dev/design/draft-fuzzing 定稿为提案 #44551。
- [Spare, 2016] Caleb Spare. Proposal: testing: better support test helper functions with TB.Helper. Go design document, 2016-12-27. https://go.dev/design/4899-testing-helper 随 Go 1.9 发布。
- [van Lohuizen, 2015] Marcel van Lohuizen. Proposal: testing: programmatic sub-test and sub-benchmark support. Go design document, 2015-09-02. https://go.dev/design/12166-subtests
- 本书 16.5 基准测试与性能剖析(
b.Loop、b.N收敛与内存度量的机制)、16.1 运行时死锁检查、16.2 竞争检查。