现在的项目开发从来不是一个人可以完成的,需要多人协作,在多人协作过程中如何保证代码的质量,如何优化代码的性能等,就让我们一起来看一下单元测试和基准测试。
单元测试
开发完功能后,如果直接把代码合并到代码库,用于上线或者被他人使用,不经过测试的代码逻辑可能会存在问题,如果强行合并,可能会影响到他人开发, 若强行上线,可能导致线上Bug、影响用户使用。
什么是单元测试
顾名思义,单元测试强调的是对单元进行测试。在开发中,一个单元可以是一个函数、一个模块等。一般情况下,要测试的单元应该是一个完整的最小单元,比如Go语言的函数。这样的话,当每个最小单元都被验证通过,那么整个模块、甚至整个程序都可以被验证通过。 单元测试由开发者自己编写,即谁改动了代码,谁要编写对应的单元测试代码以验证本次改动的正确性。
Go语言的单元测试
虽然每种编程语言里单元测试的概念是一样的,但是对单元测试的设计不一样。Go语言也有自己的单元测试规范。
go test -v ./test
运行该命令,可以执行test目录下所有单元测试。如果我们看到PASS标记,说明 单元测试通过,而且还可以看到在单元测试中写的日志。Go语言测试框架可以让我们很容易的进行单元测试,但是需要遵循5点规则:
1. 含有单元测试的 go文件必须有_test.go 结尾,Go语言测试工具只认符合这个规则的文件。
1. 单元测试文件名_test.go 前面的部分最好是被测试的函数所在的go文件的文件名。
1. 单元测试的函数名必须以Test开头,是可导出的、公开的函数。
1. 测试函数的签名必须接收一个指向 testing.T 类型的指针,并且不能返回任何值。
1. 函数名最好是 Test + 要测试的函数名。
遵循以上规则, 就可以很容易的编写单元测试了。单元测试的重点在于熟悉业务代码的逻辑,场景等,以便尽可能全面测试,保障代码质量。
单元测试覆盖率
Go语言提供了非常方便的命令来查看单元 测试覆盖率。
go test -v --covereprofile=test.cover ./test
// 上述命令包含 --coverprofile 这个Flag,它可以得到一个单元测试覆盖率文件,运行这个命令还可以同时看到测试覆盖率。
// 输出结果
PASS
coverage:87% of statements
ok xxx/test 0.27s coverage:87% of statements
// 运行如下命令,可以得到一个HTML格式的单元测试覆盖率报告:
go tool cover -html=test.cover -o=test.html
// 运行后,会在当前目录下生成一个test.html文件,使用浏览器打开,就可以看到结果。
红色标记的部分是没有测试到的,绿色标记的部分是已经测试到的。这就是单元测试覆盖率报告的好处,通过它可以很容易的检测自己写的测试是否完全覆盖。
基准测试
除了需要保证编写的代码逻辑正确外,有时候还有性能要求。如何衡量代码的性能呢? 这需要基准测试。
什么是基准测试 基准测试(Benchmark)是一项用于测量和评估软件性能指标的方法,主要用于评估代码的性能。
Go语言的基准测试
Go语言的基准测试和单元测试规则基本一样,只是测试函数的命名规则不一样。
func BenchmarkTest(b *testing.B){
for i:=0; i<b.N; i++{
// 代码逻辑
}
}
基准测试示例如上,它和单元测试的不同点在于:
1. 基准测试函数必须以 **Benchmark** 开头,必须是可导出的。
1. 函数的签名必须接收一个 *testing.B 类型的指针,并且不能返回任何值。
1. 最后的for循环很重要,被测试的代码要放到循环里。
1. b.N 是基准测试框架提供的,表示循环的次数,因为需要反复调用测试的代码,才可以评估性能。
写好了基准测试,就可以通过如下命令测试性能:
go test -bench=. ./test
goos: darwin
goarch: amd64
pkg : mytest/test
BenchmarkTest-8 3727612 343 ns/op
PASS
ok mytest/test 2.120s
// 运行基准测试也要使用 go test 命令,不过要加上 -bench 这个Flag,它接收一个表达式作为参数,以匹配基准测试的函数, “.” 表示运行所有基准测试。
// 输出结果中, 函数后面的 -8 ,表示运行基准测试时对应的 GOMAXPROCS 的值。 接着 3727612 表示运行for循环的次数,也就是调用被测试代码的次数,最后的 343 ns/op 表示每次需要 343纳秒。
// 基准测试的时间默认是1秒,也就是1秒调用3727612次,每次花费 343纳秒。 如果想让测试运行的时间更长,可以通过 -benchmark 指定,比如3秒:
go test -bench=. -benchmark=3s ./test
在运行go test时,使用 -benchmem 这个Flag 进行内存统计,通过-benchmem 查看内存的方法适用于所有的基准测试用例,其命令如下:
go test -bench=. -benchmem ./test
计时方法
运行基准测试之前会做一些准备,比如构建测试数据等,这些准备雅瑶消耗时间,所以需要把这部分时间排除在外,这就需要通过 ResetTimer 方法重置计时器。示例如下:
func BenchmarkTest(b *testing.B) {
n := 10
b.ResetTimer() // 重置计时器
for i:= 0; i < b.N; i++ {
// 代码逻辑
}
}
// 这样可以避免因为准备数据耗时造成的干扰。 除了ResetTime 方法外,还有StartTimer 和 StopTimer 方法,帮助我们灵活控制什么时候开始计时,什么时候停止计时。
内存统计
在基准测试时,还可以统计每次操作分配内存的次数,以及每次操作分配的字节数,这两个指标可以作为优化代码的参考。要开始内存统计也比较简单,代码如下,即通过 ReportAllocs() 方法:
func BenchmarkTest(b *testing.B) {
n := 10
b.ReportAllocs() // 开启内存统计
b.ResetTimer() // 重置计时器
for i:=0; i <b.N; i++ {
// 代码逻辑
}
}
go test -bench=. ./test
goos: darwin
goarch: amd64
pkg : mytest/test
BenchmarkTest-8 2317612 457 ns/op 0 B/op 0 allocs/op
PASS
ok mytest/test 2.120s
// 可以看到比原来的基准测试多了两个指标,分别是 0 B/op 和 0 allocs/op。前者便是每次操作分配了多少字节的内存,后者表示每次操作分配内存的次数。这两个指标可以作为代码优化的参考,尽可能的越小越好。
提示: 以上两个指标是否是越小越好? 这也不一定的,因为有时候需要空间换时间,所以要根据自己的具体业务而定,做到满足业务的情况下越小越好。
并发基准测试
除了普通的基准测试外,Go语言还支持并发基准测试,可以测试在多个goroutine 并发下代码的性能。并发基准测试代码如下:
func BenchmarkTestRunParallel (b *testing.B){
n := 10
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
// 代码逻辑
}
})
}
// 可以看到Go语言通过 RunParallel 方法进行并发基准测试。RunParallel 方法会创建多个goroutine,并将 b.N 分配给这些 goroutine执行。
基准测试实战
编写一个经典的斐波那契数列。在递归运算中,一定会有重复计算,这是影响递归的主要因素。解决重复计算可以使用缓存,把已经计算好的结果缓存起来,就可以重复使用了。
// 缓存已经计算好的结果
var cache= map[int]int{}
func Fibonacci(n int) int {
if v, ok := cache[n]; ok {
return v
}
result := 0
switch {
case n < 0:
result = 0
case n ==0:
result = 0
case n == 1:
result = 1
default :
result = Fibonacci(n-1) + Fiboncci(n-2)
}
cache[n] = result
return result
}
// 这组代码的核心在于采用一个map将已经计算好的结果缓存、便于重新使用。改造后,代码执行效率很高,性能大幅提升。
BenchmarkTest-8 97823192 11.7 ns/op
// 从结果可以看到,11.7纳秒,相比之前性能大幅提升。
总结:
单元测试是保证代码质量的好方法,但单元测试也不是万能的,使用它可以降低Bug率,但也不要完全依赖。除了单元测试外,还可以辅助Code Review、人工测试等手段更好的保证代码质量。