go语言的测试
不写测试的开发不是好程序员。
go test 工具
go语言测试依赖go test
命令。编写测试代码和编写普通的go代码过程是类似的。
在包目录中,所有以_test.go
为后缀名的源码文件都是go test
测试的一部分,不会被go build
编译到最终的可执行文件中。
在*_test.go
文件中有三种类型的函数,单元测试函数、基准测试函数、示例函数。
类型 | 格式 | 作用 |
---|---|---|
测试函数 | 前缀为Test | 测试程序的一些逻辑行为是否正确 |
基准函数 | 前缀为Benchmark | 测试函数的性能 |
示例函数 | 前缀为Example | 问文档提供示例文档 |
测试函数
测试函数的格式
1 | func TestName(t *testing.T) { |
测试函数的示例
需要测试的代码
1 | package split |
编写测试函数
1 | package split |
测试所有
写完测试的函数后,可以在当前包下用go tes
t开始测试。默认会测试所有的测试函数:
1 | jiang_wei@master01:~/code/go/src/golang/study/split$ go test |
测试的详细信息
但是,这样看不出详细信息,可以加参数-v
:
1 | jiang_wei@master01:~/code/go/src/golang/study/split$ go test -v |
测试单个
当我们只想测试一个功能时,可以加-run
or --run
:
1 | jiang_wei@master01:~/code/go/src/golang/study/split$ go test -v -run Split2 |
这个功能是有问题的,当我们的切割符是多个字符时,会测试失败。
添加测试函数:
1 | // 如果切割的字符串是多个字符 |
测试:
1 | jiang_wei@master01:~/code/go/src/golang/study/split$ go test -v -run Split3 |
可以看出测试驱动开发,修改我们的代码。
1 | // 如果切割的字符串是多个字符 |
继续测试:
1 | jiang_wei@master01:~/code/go/src/golang/study/split$ go test -v |
测试组
将多个测试用例放到一起就是测试组。
上面我们测试了三个功能,用了三个函数,这次我们只编写一个测试函数来完成所有的测试。
1 | package split |
测试:
1 | jiang_wei@master01:~/code/go/src/golang/study/split$ go test -v |
子测试
上面的测试组也挺方便,但是当测试用例过多的时候,无法一目了然看出哪个测试失败,无法控制我执行某个测试用例。在go 1.7+
版本中加入了子测试。我们可以按照如下方式使用t.Run
执行子测试:
1 | package split |
这样做有什么好处呢?看看就知道了。
1 | jiang_wei@master01:~/code/go/src/golang/study/split$ go test -v |
是不是更详细了,还有更好玩的:
1 | jiang_wei@master01:~/code/go/src/golang/study/split$ go test --run /split3 -v |
可以看出,和我们之前编写的三个测试用例一样,可以单独对一个用例进行测试。
测试覆盖率
测试覆盖率是你的代码被测试套件覆盖的百分比。通常我们使用的都是语句的覆盖率,也就是在测试中至少被运行一次的代码占总代码的比例。
1 | jiang_wei@master01:~/code/go/src/golang/study/split$ go test -cover |
从上面的结果可以看到我们的测试用例覆盖了100%的代码。
我们在我们的功能代码中写一个与我们的测试模块功能无关的函数,看看测试覆盖率变成了多少:
1 | func Add(a, b int) int { |
1 | jiang_wei@master01:~/code/go/src/golang/study/split$ go test -cover |
另外一些功能
- 将覆盖率的日志记录输出到文件:
go test -cover -coverprofile=c.out
- 以html方式打开上一步生成的文件:
go tool cover -html=c.out
基准测试
测试性能
基准测试函数格式
1 | func BenchmarkName(b *testing.B){ |
基准测试示例
1 | package split |
测试:
1 | jiang_wei@master01:~/code/go/src/golang/study/split$ go test -bench=Split |
我们还可以为基准测试添加-benchmem
参数,来获得内存分配的统计数据。
1 | jiang_wei@master01:~/code/go/src/golang/study/split$ go test -bench=Split -benchmem |
其中,112 B/op
表示每次操作内存分配了112字节,3 allocs/op
则表示每次操作进行了3次内存分配。 我们将我们的Split
函数优化如下:
1 | package split |
这一次我们提前使用make函数将result初始化为一个容量足够大的切片,而不再像之前一样通过调用append函数来追加。我们来看一下这个改进会带来多大的性能提升:
测试:
1 | jiang_wei@master01:~/code/go/src/golang/study/split$ go test -bench=Split -benchmem |
这个使用make函数提前分配内存的改动,减少了2/3的内存分配次数,并且减少了一半的内存分配。
性能比较函数
编写斐波拉契函数:
1 | package fib |
编写性能比较函数:
1 | package fib |
测试:
1 | jiang_wei@master01:~/code/go/src/golang/study/fib$ go test -bench=. |
重置时间
b.ResetTimer
之前的处理不会放到执行时间里,也不会输出到报告中,例如一些连接数据库的操作,不应该计算在内的,所以可以在之前做一些不计划作为测试报告的操作。例如:
1 | func BenchmarkSplit(b *testing.B) { |
并行测试
RunParallel
会创建出多个goroutine
,并将b.N
分配给这些goroutine
执行, 其中goroutine
数量的默认值为GOMAXPROCS
。用户如果想要增加非CPU受限(non-CPU-bound)基准测试的并行性, 那么可以在RunParallel
之前调用SetParallelism
。
1 | package split |
测试:
1 | jiang_wei@master01:~/code/go/src/golang/study/split$ go test -bench=. -v |
Setup与TearDown
测试程序有时需要在测试之前进行额外的设置(setup)或在测试之后进行拆卸(teardown)。
比如说测试之前进行数据库的连接。
TestMain
1 | package split |
测试:
1 | jiang_wei@master01:~/code/go/src/golang/study/split$ go test -v |
组测试的Setup与TearDown
有时候我们可能需要为每个测试集设置Setup与Teardown:
编写测试函数:
1 | package split |
测试:
1 | jiang_wei@master01:~/code/go/src/golang/study/split$ go test -v |
子测试的Setup与TearDown
有可能需要为每个子测试设置Setup与Teardown。
1 | package split |
测试:
1 | jiang_wei@master01:~/code/go/src/golang/study/split$ go test -v |
示例函数
下面的代码是我们为Split
函数编写的一个示例函数:
1 | package split |
测试:
1 | jiang_wei@master01:~/code/go/src/golang/study/split$ go test -run Split -v |
修改Output
:
1 | package split |
测试:
1 | jiang_wei@master01:~/code/go/src/golang/study/split$ go test -run Split -v |