单元测试
**单元测试:顾名思义针对单元进行测试,一个单元指的是函数、模块等;
当每个最小单元都被测试通过,那么整个模块甚至整个程序都是可以被验证通过;
#单元测试 需要遵循五点规则。
-
含有单元测试代码的 go 文件必须以 _test.go 结尾,Go 语言测试工具只认符合这个规则的文件。
-
单元测试文件名 _test.go 前面的部分最好是被测试的函数所在的 go 文件的文件名,比如以上示例中单元测试文件叫 main_test.go,因为测试的 Fibonacci 函数在 main.go 文件里。
-
单元测试的函数名必须以 Test 开头,是可导出的、公开的函数。
-
测试函数的签名必须接收一个指向 testing.T 类型的指针,并且不能返回任何值。
-
函数名最好是 Test + 要测试的函数名,比如例子中是 TestFibonacci,表示测试的是 Fibonacci 这个函数。
单元测试的重点在于熟悉业务逻辑、 场景等,以便尽可能全面的测试,确保代码质量。
func Fibonacci(n int) int {
if n < 0 {
return 0
}
if n == 0 {
return 0
}
if n == 1 {
return 1
}
return Fibonacci(n-1) + Fibonacci(n-2)
}
单元测试覆盖率:
$ go test -v --coverprofile=ch18.cover ./ch18
PASS
coverage: 85.7% of statements
ok gotour/ch18 0.367s coverage: 85.7% of statements
-coverprofile 这个 Flag,它可以得到一个单元测试覆盖率文件,运行这行命令还可以同时看到测试覆盖率
测试覆盖率为 85.7%。从这个数字来看,应该没有被全面地测试,这时候就需要查看详细的单元测试覆盖率报告了。
➜ go tool cover -html=ch18.cover -o=ch18.html
命令运行后,会在当前目录下生成一个 ch18.html 文件,使用浏览器打开它: 红色标记的部分是没有测试到的;
这就是单元测试覆盖率报告的好处,通过它你可以很容易地检测自己写的单元测试是否完全覆盖。
结合CI做单元测试及覆盖率的报告
实际工作结合阿里云code up做的测试报告:
# 默认的单元测试命令
# 输出测试报告目录到当前工作目录,可自动上传并展示
mkdir -p test-report-unit
# 如果有集成测试使用`go list ./... | grep -v test`排除test目录,如果没有集成测试使用./...运行所有单元测试
go test -v -json -cover -coverprofile test-report-unit/cover.out `go list ./... | grep -v test` > test-report-unit/report.jsonl
go tool cover -html=test-report-unit/cover.out -o test-report-unit/index.html
# 上述命令生产的目录和报告文件需要与测试报告目录、测试报告文件、测试报告入口文件匹配,否则无法生成报告结果
[[基准测试]]
#基准测试 (Benchmark)是一项用于测量和评估软件性能指标的方法,主要用于评估你写的代码的性能。
# -cpu n 指定cpu的核数
# -v 指定目录
# -benchmem 开启内存统计
go test -bench=BenchmarkIntersect -v roaring-bitmap_test.go -cpu=1,2,4,6,8,10 -benchmem
# 结果
BenchmarkIntersect 4263(执行次数) 269046 ns/op(每次运行时间) 406067 B/op (每次耗用内存) 9 allocs/op (每次分配内存次数)
BenchmarkIntersect-2 5684 226401 ns/op 405001 B/op 9 allocs/op
BenchmarkIntersect-4 7095 186264 ns/op 404367 B/op 9 allocs/op
BenchmarkIntersect-6 6895 164794 ns/op 404441 B/op 9 allocs/op
BenchmarkIntersect-8 7218 162620 ns/op 404323 B/op 9 allocs/op
BenchmarkIntersect-10 6753 165705 ns/op 404497 B/op 9 allocs/op
PASS
ok codeup.aliyun.com/qimao/bigdata/rec/internal/pkg/book_attr_bitmap 12.678s
func BenchmarkFibonacci(b *testing.B){
for i:=0;i<b.N;i++{
Fibonacci(10)
}
}
-
基准测试函数必须以 Benchmark 开头,必须是可导出的;
-
函数的签名必须接收一个指向 testing.B 类型的指针,并且不能返回任何值;
-
最后的 for 循环很重要,被测试的代码要放到循环里;
-
b.N 是基准测试框架提供的,表示循环的次数,因为需要反复调用测试的代码,才可以评估性能。
基准测试:
➜ go test -bench=. ./ch18
goos: darwin
goarch: amd64
pkg: gotour/ch18
BenchmarkFibonacci-8 3461616 343 ns/op
PASS
ok gotour/ch18 2.230s
-bench 这个 Flag,它作为参数,以匹配基准测试的函数,”.”表示运行所有基准测试。该参数是一个正则表达式,用于匹配要执行的基准测试函数的名字,默认值是空的。
函数后面的 -8 表示运行基准测试时对应的 GOMAXPROCS 的值。接着的 3461616 表示运行 for 循环的次数,也就是调用被测试代码的次数,最后的 343 ns/op 表示每次需要花费 343 纳秒。
基准测试的时间默认是 1 秒,也就是 1 秒调用 3461616 次、每次调用花费 343 纳秒。如果想让测试运行的时间更长,可以通过 -benchtime 指定,比如 3 秒,代码如下所示:
$ go test -bench=. -benchtime=3s ./ch18
计时方法
进行基准测试之前会做一些准备,比如构建测试数据等,这些准备也需要消耗时间,所以需要把这部分时间排除在外。这就需要通过 ResetTimer 方法重置计时器:
func BenchmarkFibonacci(b *testing.B) {
n := 10
b.ResetTimer() //重置计时器
for i := 0; i < b.N; i++ {
Fibonacci(n)
}
}
避免因为准备数据耗时造成的干扰。除了 ResetTimer 方法外,还有 StartTimer 和 StopTimer 方法,帮你灵活地控制什么时候开始计时、什么时候停止计时。
内存统计
在基准测试时,还可以统计每次操作分配内存的次数,以及每次操作分配的字节数,这两个指标可以作为优化代码的参考。要开启内存统计也比较简单,代码如下,即通过 ReportAllocs() 方法,与 go test 使用 -benchmem 标志类似,但 ReportAllocs 只影响那些调用了该函数的基准测试。
func BenchmarkFibonacci(b *testing.B) {
n := 10
b.ReportAllocs() //开启内存统计
b.ResetTimer() //重置计时器
for i := 0; i < b.N; i++ {
Fibonacci(n)
}
}
再运行这个基准测试,就可以看到如下结果:
➜ go test -bench=. ./ch18
goos: darwin
goarch: amd64
pkg: gotour/ch18
BenchmarkFibonacci-8 2486265 486 ns/op 0 B/op 0 allocs/op
PASS
ok gotour/ch18 2.533s
可以看到相比原来的基准测试多了两个指标,分别是 0 B/op 和 0 allocs/op。前者表示每次操作分配了多少字节的内存,后者表示每次操作分配内存的次数。这两个指标可以作为代码优化的参考,尽可能地越小越好。
以上两个指标是否越小越好?这是不一定的,因为有时候代码实现需要空间换时间,所以要根据自己的具体业务而定,做到在满足业务的情况下越小越好。
并发基准测试
Go 语言还支持并发基准测试,你可以测试在多个 goroutine 并发下代码的性能。还是以 Fibonacci 为例,它的并发基准测试代码如下:
func BenchmarkFibonacciRunParallel(b *testing.B) {
n := 10
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Fibonacci(n)
}
})
}
Go 语言通过 RunParallel 方法运行并发基准测试。RunParallel 方法会创建多个 goroutine,并将 b.N 分配给这些 goroutine 执行。
单元测试是保证代码质量的好方法,但单元测试也不是万能的,使用它可以降低 Bug 率,但也不要完全依赖。除了单元测试外,还可以辅以 Code Review、人工测试等手段更好地保证代码质量。
代码检查
测试包
testing
使用testing可以完成大部分单元测试需求,而为了更优雅及高效地完成单元测试,需要借助一些第三方包。
断言
标准库为我们提供了一个还不错的测试框架,但是没有提供断言的功能。
goconvey https://github.com/smartystreets/goconvey (支持原生go test)
testify https://godoc.org/github.com/stretchr/testify/assert 包含了 断言、mock、suite 三个功能,
打桩
实际的单元测试中,某个功能模块往往会有很多的依赖项:
数据库连接、文件I/O、其他函数模块、全局变量等,为了专注于主要对象的测试,我们可以使用一个模拟对象来代替次要模块,以简化测试。
- GoMock
https://github.com/golang/mock
gomock是官方提供的模拟框架,可以与testing很好的集成。主要针对的是对象+接口的数据结构。也就是框架完成了繁琐的实现接口的工作。
- gomonkey
https://github.com/agiledragon/gomonkey
https://cloud.tencent.com/developer/article/1872029
gomonkey实现了单元测试中的猴子补丁,可以很方便地为方法(成员方法也可)、全局变量打桩,同时可以指定行为序列。
Reference
https://books.studygolang.com/The-Golang-Standard-Library-by-Example/chapter09/09.2.html
Go工程化- 单元测试 https://lailin.xyz/post/go-training-week4-unit-test.html