【Go质量保证:单元测试&基准测试】

现在的项目开发从来不是一个人可以完成的,需要多人协作,在多人协作过程中如何保证代码的质量,如何优化代码的性能等,就让我们一起来看一下单元测试和基准测试

单元测试

​ 开发完功能后,如果直接把代码合并到代码库,用于上线或者被他人使用,不经过测试的代码逻辑可能会存在问题,如果强行合并,可能会影响到他人开发, 若强行上线,可能导致线上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、人工测试等手段更好的保证代码质量。


  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值