Go语言单元测试与基准测试
Go语言拥有一套单元测试和性能测试系统,仅需要添加很少的代码就可以快速测试一段需求代码。
性能测试系统可以给出代码的性能数据,帮助测试者分析性能问题。
单元测试
概述
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。
单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。
testing 提供对 Go 包的自动化测试的支持。通过 go test
命令,能够自动执行如下形式的任何函数:
func TestXxx(*testing.T)
- 测试用例文件不会参与正常源码编译,不会被包含到可执行文件中。
- 测试用例文件使用 go test 指令来执行,没有也不需要 main() 作为函数入口。所有在以
_test
结尾的源码内以Test
开头的函数会自动被执行。 - 测试用例可以不传入 *testing.T 参数。
在这些函数中,使用 Error, Fail 或相关方法来发出失败信号。
要编写一个新的测试套件,需要创建一个名称以 _test.go 结尾的文件,该文件包含 TestXxx
函数,如上所述。 将该文件放在与被测试的包相同的包中。该文件将被排除在正常的程序包之外,但在运行 “go test” 命令时将被包含。 有关详细信息,请运行 “go help test” 和 “go help testflag” 了解。
如果有需要,可以调用 *T 和 *B 的 Skip 方法,跳过该测试或基准测试:
func TestTimeConsuming(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
...
}
Go语言的单元测试对文件名和方法名,参数都有很严格的要求。
- 文件名必须以xxx_test.go命名
- 方法必须是Test[^a-z]开头
- *方法参加必须 t testing.T
- 使用go test执行单元测试
go test参数解读
go test是go语言自带的测试工具,其中包含的是两类,单元测试和性能测试
通过go help test可以看到go test的使用说明:
格式形如:
go test [-c] [-i] [build/test flags] [packages] [build/test flags & test binary flags]
参数解读:
-c : 编译go test成为可执行的二进制文件,但是不运行测试。
-i : 安装测试包依赖的package,但是不运行测试。
关于build flags,调用go help build,这些是编译运行过程中需要使用到的参数,一般设置为空
关于packages,调用go help packages,这些是关于包的管理,一般设置为空
关于flags for test binary,调用go help testflag,这些是go test过程中经常使用到的参数
-test.v : 是否输出全部的单元测试用例(不管成功或者失败),默认没有加上,所以只输出失败的单元测试用例。
-test.run pattern: 只跑哪些单元测试用例
-test.bench patten: 只跑那些性能测试用例
-test.benchmem : 是否在性能测试的时候输出内存情况
-test.benchtime t : 性能测试运行的时间,默认是1s
-test.cpuprofile cpu.out : 是否输出cpu性能分析文件
-test.memprofile mem.out : 是否输出内存性能分析文件
-test.blockprofile block.out : 是否输出内部goroutine阻塞的性能分析文件
-test.memprofilerate n : 内存性能分析的时候有一个分配了多少的时候才打点记录的问题。这个参数就是设置打点的内存分配间隔,也就是profile中一个sample代表的内存大小。默认是设置为512 * 1024的。如果你将它设置为1,则每分配一个内存块就会在profile中有个打点,那么生成的profile的sample就会非常多。如果你设置为0,那就是不做打点了。
你可以通过设置memprofilerate=1和GOGC=off来关闭内存回收,并且对每个内存块的分配进行观察。
-test.blockprofilerate n: 基本同上,控制的是goroutine阻塞时候打点的纳秒数。默认不设置就相当于-test.blockprofilerate=1,每一纳秒都打点记录一下
-test.parallel n : 性能测试的程序并行cpu数,默认等于GOMAXPROCS。
-test.timeout t : 如果测试用例运行时间超过t,则抛出panic
-test.cpu 1,2,4 : 程序运行在哪些CPU上面,使用二进制的1所在位代表,和nginx的nginx_worker_cpu_affinity是一个道理
-test.short : 将那些运行时间较长的测试用例运行时间缩短
示例:
定义一个test包,包内有一个加法和减法的函数
package test
func Sum(a int, b int) int {
return a + b
}
func Sub(a int, b int) int {
return a - b
}
测试文件的名称不需要和包文件名称一样,也可以叫abc_test.go等
测试文件test_test.go的测试代码如下
package test
import (
"testing"
)
//编写一个测试用例,去测试Sum函数是否正确
func TestSum(t *testing.T) {
res := Sum(10, 20)
if res != 30 {
t.Fatalf("Sum(10, 20)执行错误")
}
//如果正确,输出日志
t.Logf("Sum(10, 20)执行正确")
}
func TestOk(t *testing.T) {
t.Logf("这个方法也进来啦")
}
func TestSub(t *testing.T) {
res := Sub(10, 5)
if res != 5 {
t.Fatalf("Sub(10, 5)执行错误")
}
//如果正确,输出日志
t.Logf("Sub(10, 5)执行正确")
}
在终端进入该包的目录内,使用go test命令进行测试
加上-v
参数可以让测试时显示详细的流程。
我们再新建一个abc_test.go文件,把test_test.go文件里的TestSub()方法挪到abc_test.go文件中
然后重新执行go test -v命令
可以看到test_test.go内的方法都被执行了,由此可知,使用go test -v命令会把该包目录内的所有测试文件的所有方法都执行一遍。
如果想测试包里面的单个文件,一定要带上被测试的原文件,如
go test -v abc_test.go test.go
如果想测试单个方法,需要加上 -run
参数,并且方法名末尾要加上$
,原因是-run
跟随的测试用例的名称支持正则表达式,使用-run TestSub$
即可只执行 TestSub 测试用例。否则会执行所有以TestSub开头的所有函数如,修改abc_test.go
package test
import (
"testing"
)
//编写一个测试用例,去测试Sum函数是否正确
func TestSum(t *testing.T) {
res := Sum(10, 20)
if res != 30 {
t.Fatalf("Sum(10, 20)执行错误")
}
//如果正确,输出日志
t.Logf("Sum(10, 20)执行正确")
}
func TestOk(t *testing.T) {
t.Logf("这个方法也进来啦")
}
func TestSub2(t *testing.T) {
t.Logf("进入到TestSub2方法里来了")
}
go test -v -test.run TestSub$
或
go test -v -run TestSub$
如果不加$
,所有以TestSub开头的所有测试函数都会被执行
单元测试日志
每个测试用例可能并发执行,使用 testing.T 提供的日志输出可以保证日志跟随这个测试上下文一起打印输出。testing.T 提供了几种日志输出方法,详见下表所示。
方 法 | 备 注 |
---|---|
Log | 打印日志,同时结束测试 |
Logf | 格式化打印日志,同时结束测试 |
Error | 打印错误日志,同时结束测试 |
Errorf | 格式化打印错误日志,同时结束测试 |
Fatal | 打印致命日志,同时结束测试 |
Fatalf | 格式化打印致命日志,同时结束测试 |
基准测试
基准测试可以测试一段程序的运行性能及耗费 CPU 的程度。Go 语言中提供了基准测试框架,使用方法类似于单元测试,使用者无须准备高精度的计时器和各种分析工具,基准测试本身即可以打印出非常标准的测试报告。
压力测试用来检测函数(方法)的性能,和编写单元功能测试的方法类似,但需要注意以下几点:
-
压力测试用例必须遵循如下格式,其中XXX可以是任意字母数字的组合,但是首字母不能是小写字母
func BenchmarkXXX(b *testing.B) { ... }
-
go test不会默认执行压力测试的函数,如果要执行压力测试需要带上参数-test.bench,语法:-test.bench="test_name_regex",例如go test -test.bench=".*"表示测试全部的压力测试函数
-
在压力测试用例中,请记得在循环体内使用testing.B.N,以使测试可以正常的运行
-
文件名也必须以_test.go结尾
基础测试基本使用
新建一个压力测试文件bench_test.go
目录结构
bench_test.go代码
package test
import "testing"
func BenchmarkSub(b *testing.B) {
for i := 0; i < b.N; i++ { //use b.N for looping
Sub(10, 5)
}
}
func BenchmarkTimeConsumingFunction(b *testing.B) {
b.StopTimer() //调用该函数停止压力测试的时间计数
//做一些初始化的工作,例如读取文件数据,数据库连接之类的,
//这样这些时间不影响我们测试函数本身的性能
b.StartTimer() //重新开始时间
for i := 0; i < b.N; i++ {
Sub(10, 5)
}
}
这段代码使用基准测试框架测试减法性能。第 7 行中的 b.N 由基准测试框架提供。测试代码需要保证函数可重入性及无状态,也就是说,测试代码不使用全局变量等带有记忆性质的数据结构。避免多次运行同一段代码时的环境不一致,不能假设 N 值范围。
执行命令
go test -test.bench=".*"
输出结果:
goos: darwin
goarch: amd64
pkg: test
BenchmarkSub-8 2000000000 0.32 ns/op
//BenchmarkSub执行了2000000000次,每次的执行平均时间是0.32纳秒
BenchmarkTimeConsumingFunction-8 2000000000 0.31 ns/op
//BenchmarkTimeConsumingFunction,执行了2000000000次,每次的执行平均时间是0.31纳秒
PASS
ok test 1.328s
我们还可以使用-count
执行次数
go test -test.bench=".*" -count=5
输出结果:
goos: darwin
goarch: amd64
pkg: test
BenchmarkSub-8 2000000000 0.31 ns/op
BenchmarkSub-8 2000000000 0.32 ns/op
BenchmarkSub-8 2000000000 0.32 ns/op
BenchmarkSub-8 2000000000 0.31 ns/op
BenchmarkSub-8 2000000000 0.31 ns/op
BenchmarkTimeConsumingFunction-8 2000000000 0.31 ns/op
BenchmarkTimeConsumingFunction-8 2000000000 0.31 ns/op
BenchmarkTimeConsumingFunction-8 2000000000 0.31 ns/op
BenchmarkTimeConsumingFunction-8 2000000000 0.31 ns/op
BenchmarkTimeConsumingFunction-8 2000000000 0.31 ns/op
PASS
ok test 6.573s
基准测试原理
基准测试框架对一个测试用例的默认测试时间是 1 秒。开始测试时,当以 Benchmark 开头的基准测试用例函数返回时还不到 1 秒,那么 testing.B 中的 N 值将按 1、2、5、10、20、50……递增,同时以递增后的值重新调用基准测试用例函数。
自定义测试时间
通过-benchtime
参数可以自定义测试时间,例如:
go test -test.bench=".*" -count=5 -benchtime=5s
测试内存
基准测试可以对一段代码可能存在的内存分配进行统计,下面是一段使用字符串格式化的函数,内部会进行一些分配操作。
func Benchmark_Alloc(b *testing.B) {
for i := 0; i < b.N; i++ {
fmt.Sprintf("%d", i)
}
}
在命令行中添加-benchmem
参数以显示内存分配情况,参见下面的指令:
bogon:test itbsl$ go test -test.bench=Alloc -benchmem
goos: darwin
goarch: amd64
pkg: test
BenchmarkAlloc-8 20000000 111 ns/op 16 B/op 2 allocs/op
PASS
ok test 2.354s
代码说明如下:
- 第 1 行的代码中
-bench
后添加了 Alloc,指定只测试 Benchmark_Alloc() 函数。 - 第 4 行代码的“16 B/op”表示每一次调用需要分配 16 个字节,“2 allocs/op”表示每一次调用有两次分配。
开发者根据这些信息可以迅速找到可能的分配点,进行优化和调整。
控制计时器
有些测试需要一定的启动和初始化时间,如果从 Benchmark() 函数开始计时会很大程度上影响测试结果的精准性。testing.B 提供了一系列的方法可以方便地控制计时器,从而让计时器只在需要的区间进行测试。我们通过下面的代码来了解计时器的控制。
基准测试中的计时器控制
func BenchmarkAddTimerControl(b *testing.B) {
// 重置计时器
b.ResetTimer()
// 停止计时器
b.StopTimer()
// 开始计时器
b.StartTimer()
var n int
for i := 0; i < b.N; i++ {
n++
}
}
从 Benchmark() 函数开始,Timer 就开始计数。StopTimer() 可以停止这个计数过程,做一些耗时的操作,通过 StartTimer() 重新开始计时。ResetTimer() 可以重置计数器的数据。
计数器内部不仅包含耗时数据,还包括内存分配的数据。