第十一章 单元测试
- 让软件的复杂性可得到控制两个有效的方法:
- 代码在被正式部署前需要进行代码评审
- 自动化测试
- Go语言的测试技术是相对低级的,。它依赖一个
go test
测试命令和一组按照约定方式编写的测试函数,测试命令可以运行这些测试函数
11.1go test
划重点
go test
命令是一个按照一定的约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go
为后缀名的源文件并不是go build
构建包的一部分,它们是go test
测试的一部分。- 在*_test.go文件中,有三种类型的函数:测试函数、基准测试函数、示例函数。
- 测试函数是以Test为函数名前缀的函数,用于测试程序的一些逻辑行为是否正确;
go test
命令会调用这些测试函数并报告测试结果是PASS
或FAIL
。 - 基准测试函数是以Benchmark为函数名前缀的函数,它们用于衡量一些函数的性能;
go test
命令会多次运行基准函数以计算一个平均的执行时间 - 示例函数是以Example为函数名前缀的函数,提供一个由编译器保证正确性的示例文档
go test
命令会遍历所有的*_test.go
文件中符合上述命名规则的函数,然后生成一个临时的main
包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。
11.2测试函数
划重点
- 每个测试函数必须导入testing包。测试函数有如下的签名:
func TestName(t *testing.T) {
// ...
}
- 测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头,其中t参数用于报告测试失败和附加的日志信息:
func TestSin(t *testing.T) { /* ... */ }
func TestCos(t *testing.T) { /* ... */ }
func TestLog(t *testing.T) { /* ... */ }
go test
命令如果没有参数指定包那么将默认采用当前目录对应的包(和go build
命令一样)go test -v
,参数-v
可用于打印每个测试函数的名字和运行时间go test -run
, 参数-run
对应一个正则表达式,只有测试函数名被它正确匹配的测试函数才会被go test
测试命令运行- 失败测试的输出并不包括调用
t.Errorf
时刻的堆栈调用信息。和其他编程语言或测试框架的assert
断言不同,t.Errorf
调用也没有引起panic
异常或停止测试的执行 - 可以使用
t.Fatal
或t.Fatalf
停止当前测试函数。它们必须在和测试函数同一个goroutine
内调用
常用库及方法
(*testing.T).Error
(*testing.T).Errorf
11.2.1随机测试
划重点
- 随机测试是通过构造更广泛的随机输入来测试探索函数的行为
- 两种处理策略帮助获取希望的结果:
- 第一个是编写另一个对照函数
- 第二种是生成的随机输入的数据遵循特定的模式,
常用库及方法
time.Now().UTC().UnixNano()
rand.New(rand.NewSource(seed))
(*testing.T).Logf
11.2.2测试一个命令
划重点
go test
可以用来测试可执行程序,因为main
包可以作为一个包被测试器代码导入。注意的此时的测试代码和产品代码在同一个包。- 对于
main
包,也有对应的main
入口函数,但是在测试的时候main
包只是TestEcho
测试函数导入的一个普通包,里面main
函数并没有被导出,而是被忽略的。
11.2.3白盒测试
划重点
- 黑盒测试只需要测试包公开的文档和API行为,内部实现对测试代码是透明的
- 白盒测试(clear box)有访问包内部函数和数据结构的权限
- 处理模式可以用来暂时保存和恢复所有的全局变量,包括命令行标志参数、调试选项和优化参数;安装和移除导致生产代码产生一些调试信息的钩子函数;还有有些诱导生产代码进入某些重要状态的改变,比如超时、错误,甚至是一些刻意制造的并发行为等因素。
go test
命令并不会同时并发地执行多个测试
常用库及方法
smtp.PlainAuth
smtp.SendMail
11.2.4扩展测试包
划重点
- 可以通过测试扩展包的方式解决循环依赖的问题
- 扩展测试包可以更灵活的编写测试,特别是集成测试(需要测试多个组件之间的交互)
go list
命令查看包对应目录中哪些Go源文件是产品代码,哪些是包内测试,还哪些测试扩展包。我们以fmt
包作为一个例子:GoFiles
表示产品代码对应的Go源文件列表;也就是go build
命令要编译的部分TestGoFiles
表示的是fmt包内部测试测试代码,以_test.go
为后缀文件名- XTestGoFiles表示的是属于测试扩展包的测试代码,也就是fmt_test包
$ go list -f={
{.GoFiles}} fmt
[doc.go format.go print.go scan.go]
-----------------------------------
$ go list -f={
{.TestGoFiles}} fmt
[export_test.go]
-----------------------------------
$ go list -f={
{.XTestGoFiles}} fmt
[fmt_test.go scan_test.go stringer_test.go]
11.2.5编写有效的测试
划重点
- 测试不仅报告调用的具体函数、它的输入和结果的意义;并且打印的真实返回的值和期望返回的值;并且即使断言失败依然会继续尝试运行更多的测试。
11.2.6避免的不稳定的测试
划重点
11.3测试覆盖率
划重点
- 由测试驱动触发运行到的被测试函数的代码数目称为测试的覆盖率
- 语句的覆盖率是指在测试中至少被运行一次的代码占总代码数的比例。
go test
命令中集成的测试覆盖率工具,可以用来度量代码的测试覆盖率,帮助我们识别测试和我们期望间的差距。go tool cover
显示coverage的使用方法。go tool
命令运行Go工具链的底层可执行程序。这些底层可执行程序放在$GOROOT/pkg/tool/${GOOS}_${GOARCH}
目录。go test -run=Coverage -coverprofile=c.out gopl.io/ch7/eval
go test -cover
生成摘要-covermode=count
标志参数,那么将在每个代码块插入一个计数器而不是布尔标志量。在统计结果中记录了每个块的执行次数,这可以用于衡量哪些是被频繁执行的热点代码。$ go tool cover -html=c.out
打印了测试日志,生成一个HTML报告
11.4基准测试
划重点
- 基准测试函数和普通测试函数写法类似,但是以
Benchmark为
前缀名,并且带有一个*testing.B
类型的参数;*testing.B
参数除了提供和*testing.T
类似的方法,还有额外一些和性能测量相关的方法。它还提供了一个整数N
,用于指定操作执行的循环次数。 - 默认情况下不运行任何基准测试。
我们需要通过-bench
命令行标志参数手工指定要运行的基准测试函数。该参数是一个正则表达式,用于匹配要执行的基准测试函数的名字,默认值是空的。其中“.”
模式将可以匹配所有基准测试函数。go test -bench=.
-benchmem
命令行标志参数将在报告中包含内存的分配数据统计
11.5剖析
划重点
- CPU分析文件标识了函数执行时所需要的CPU时间。当前运行的系统线程在每隔几毫秒都会遇到操作系统的中断事件,每次中断时都会记录一个分析文件然后恢复正常的运行。
- 堆分析则记录了程序的内存使用情况。每个内存分配操作都会触发内部平均内存分配例程,每个512KB的内存申请都会触发一个事件。
- 阻塞分析则记录了goroutine最大的阻塞操作,例如系统调用、管道发送和接收,还有获取锁等。分析库会记录每个goroutine被阻塞时的相关操作。
$ go test -cpuprofile=cpu.out $ go test -blockprofile=block.out $ go test -memprofile=mem.out $ go tool pprof
- 基准测试会默认包含单元测试,这里我们用
-run=NONE
参数禁止单元测试。(这一部分需要具体查看原文的使用介绍)
11.6示例函数
划重点
- 示例函数没有函数参数和返回值。
- Example示例函数将是包文档的一部分。
- 示例函数有三个用处:
- 最主要的一个是作为文档。
- 在
go test
执行测试的时候也运行示例函数测试。测试结果会和函数内// Output:
格式的注释相比较,检查结果是否匹配。 - 提供一个真实的演练场。
后面两节暂不做笔记,普通Go程序员暂时用不上