::: hljs-center

https://p4.itc.cn/q_70/images03/20210302/5f41916b1f4f43e28cf88e71b2fb7da6.gif

:::
::: hljs-center

单元测试

:::

在学习阶段练手的项目通常会直接丢到环境上去跑,有了BUG就现修;在企业开发时,一个小小的BUG可能会导致大量的损失,因此在程序上线前对程序进行测试时是必不可少的。

image.png
测试类型分为 回归测试,集成测试,单元测试

回归测试是指在发生修改之后重新测试先前的测试以保证修改的正确性。理论上,软件产生新版本,都需要进行回归测试,验证以前发现和修复的错误是否在新软件版本上再次出现;集成测试则是使用一些自动化的工具,进行多次的回归测试操作

单元测试则是在代码维度,如开发时对函数进行测试。从上由下的的覆盖率和成本是逐步降低的,所以说单元测试的覆盖率在很高程度上决定着质量

单元测试的组成部分

image.png

单元测试的组成部分包括: 输入,测试单元,输出,与期望输出的校对 测试单元的包括较为宽泛,如包含: 函数,接口,模块,聚合的一些大函数;通过输出与期望值校对来反应是否和预期的效果相符

通过多个单元测试我们可以保证代码的质量,每次对新的模块进行单元测试,一方面可以保证新模块的正确性,当整体跑通时证明新的模块并没有影响旧模块的正确性(旧的代码也做了单元测试)

此外单元测试在一定程度下会提升效率,进行单元测试可以很快速的定位到问题,进而防止上线后损失扩大化

单元测试的规则

image.png

  1. 所有测试文件以 _test.go 结尾,这样可以方便区分定位
  2. 测试函数以驼峰的形式Test开头,如TestTakePoint
  3. 初始化逻辑放在TestMain中

以下是一个单元测试的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package Test

import "testing"

func HelloZyy() string {
return "zsh"
}

func TestHelloZyy(t *testing.T) {
output := HelloZyy()
expectOutput := "zyy"
if output != expectOutput {
t.Errorf("Expected %s do not match actual %s", expectOutput, output)
}
}

代码中HelloZyy是我们的测试单元,expectOutput为期望值
output为输出,if内条件为校对
使用go test HelloZyy_test.go 进行单元测试
结果是
image.png
显然,这次测试结果是FAIL

测试的覆盖率
那么如何衡量代码是否经过了足够的测试以及如何评价项目的测试水准呢?那便是代码覆盖率
代码覆盖率越完备对代码正确性越能有保证,直接看一个例子

judgement.go:

1
2
3
4
5
6
7
8
9
package Test

func JudgePassLine(score int16) bool {
if score >= 60 {
return true
}
return false
}

judgement_test.go

1
2
3
4
5
6
7
8
9
10
11
package Test

import (
"github.com/stretchr/testify/assert"
"testing"
)

func TestJudgePassLine(t *testing.T) {
isPass := JudgePassLine(70)
assert.Equal(t, true, isPass)
}

assert是testify库提供的“断言”功能,有了它不用再使用标准库中testing来编写各种条件判断
直接在控制台输入go get -u github.com/stretchr/testify 以安装库

1
assert.Equal(t, true, isPass)

在这行代码中,true是我们的期望值
使用指令 go test judgement_test.go judgement.go –cover来查询覆盖率
image.png
可以看到coverage为66.7%,这个数值是怎么得出来的呢?我们往测试单元JudgePassLine函数中传入了参数70
它会执行

1
2
3
if score >= 60 {
return true
}

这在函数所有代码中占了2/3,覆盖率自然就是66.7%了;接下来修改一下judgement_test.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package Test

import (
"github.com/stretchr/testify/assert"
"testing"
)

func TestJudgePassLineTrue(t *testing.T) {
isPass := JudgePassLine(70)
assert.Equal(t, true, isPass)
}

func TestJudgePassLineFail(t *testing.T) {
isPass := JudgePassLine(59)
assert.Equal(t, true, isPass)
}

在新的逻辑中,JudgePassLine()函数所有的代码都会执行一次,再来看看覆盖率
image.png
这下是100%了
实际项目开发中对覆盖率到达100%是可望不可及的,以下是一个评判覆盖率的标准

  1. 一般覆盖率50~60%;较高覆盖率80%+
  2. 测试分支相互独立,全面覆盖
  3. 测试单元粒度足够小,函数单一职责

当达到一般覆盖率时,在一定程度上是可以保证主流程是没有问题的,但一些异常的分支是没有覆盖到的;对于资金类的业务(如转账等)对覆盖率的要求会更高,在字节会要求达到85%以上

基准测试
有时候我们需要对代码进行优化或进行性能分析,go的标准库提供了基准测试的能力
以下代码模拟了负载均衡的场景,在十个服务器中随机返回一个,对Select方法进行非并行和并行的基准测试

randomSelectServer.go:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package Test

import "math/rand"

var ServerIndex [10]int

func InitServerIndex() {
for i := 0; i < 10; i++ {
ServerIndex[i] = i + 100
}
}
func Select() int {
return ServerIndex[rand.Intn(10)]
}

randomSelectServer_test.go:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package Test

import "testing"

func BenchmarkSelect(b *testing.B) {
InitServerIndex()
b.ResetTimer() // 重置计时器(因为InitServerIndex不在测试范围内)
for i := 0; i < b.N; i++ {
Select()
}
}
func BenchmarkSelectParallel(b *testing.B) {
InitServerIndex()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Select()
}
})
}

注意: 基准测试文件命名存放与单元测试一样,但在_test.go结尾文件中,测试函数需以Benchmark开头驼峰命名
分别启动两个测试方法:
image.png
运行,可以很清晰的看到函数性能和执行次数