go中测试既有类似有
pytest
中的功能测试,也有
benchMark
的基准测试,以及单元测试(
Unit Tests
,
UT
).这里从单元测试UT引入本篇的话题,单元测试的重要性不言而喻,尤其在大型项目中跨团队合作时,无法mr合格的代码,很容易影响整个团队的交付进度和质量。或者会说直接debug,但是当你的代码是几千行的时候,这个时候debug似乎也比较累,那单元测试就能覆盖上述情况。
如何写好单元测试呢?
测试用例编写是基础。比如如何编写单个测试函数和单个测试方法,如何做基准测试,如何
Mock
数据等等,对于可测试的代码,高内聚,低耦合是软件工程的基本要求。同样对于测试而言,函数和方法写法不同,测试难度也是不一样的,参数少且类型单一,与其他函数耦合度较低这种函数更容易测试,其他的比如入参较多,且耦合多较高,是输入参数较多这种情况,结合正交法,等价类划分等设计方法,可以大大较少case设计的难度和复杂度,且能提升测试覆盖面。
这里概述一下go中支持的三种测试类型,分别是
单元/功能测试
,
性能(压力)测试
,
覆盖率测试
接下来介绍使用Go的标准测试库
testing
进行单元测试
1.单元测试
go语言中推荐测试文件和源代码文件放置在一起,测试文件以
_test.go
结尾,这里可以和pytest进行类比,当前的
package
有
calculate.go
文件,向测试
calculate.go
中有
Add
和
Mul
函数,应该新建一个
calculate_test.go
测试文件,所以习惯会将测试文件命名为功能文件_test.go的形式。
1.1 入门示例
编写go test测试函数时,如下所示,输入test,自动联想
单元测试文件可以有多个测试用例组成,每个测试用例的名称前缀必须是Test开头
funcTestXxx( t \*testing.T ){//...... }
函数和单测的文件如下:
# calculate.gopackage calculate
funcAdd(a,b int)int{return a+b
}funcMult(a,b int)int{return a *b
}
# calculate_test.gopackage calculate
import"testing"funcTestAdd(t *testing.T){if ans :=Add(2,3); ans !=5{
t.Errorf("2+3 expected be 5,but %d got",ans)}if ans :=Add(-10,-20); ans !=-30{
t.Errorf("-10 + -20 expected be -30, but %d got", ans)}}
在goland终端执行go test
xxx@yyy calculate % ls
calculate.go calculate_test.gogo.mod
xxx@yyy calculate %go test -v
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok calculate 0.137s
go test -v
,
-v
参数会显示每个用例的测试结果。如果继续编写mul的测试函数.此时calculate_test.go中两个测试函数,如果想仅仅指定其中一个测试用例,比如
TestMul
,可以使用
-run
参数
go test -run TestMul -v
=== RUN TestMul
--- PASS: TestMul (0.00s)
PASS
ok calculate 0.531s
go test <module name>/<package name>
用来运行某个
package
内的所有测试用例。
运行当前
package
内的用例:
go test calcuate
或
go test .
运行子
package
内的用例:
go test calcuate/<package name>
或
go test ./<package name>
如果想递归测试当前目录下的所有的
package:go test ./...
或
go test calcuate/...
该参数还支持通配符
*
,和部分正则表达式,如
^
和
$
1.2 子测试
这里就不得不提到子测试,即所谓的Subtests,该功能是go语言内置的支持的功能,可以在一个测试用例中,根据测试场景使用
t.Run
创建不同的子测试用例:
funcTestMul(t *testing.T){//if ans := Mul(2,3); ans != 6{// t.Errorf("2*3 expected be 6,but %d got",ans)//}
t.Run("pos",func(t *testing.T){ifMul(2,3)!=6{
t.Fatalf("2*3 expected be 6, but %d got",Mul(2,3))}})
t.Run("neg",func(t *testing.T){ifMul(2,-3)!=-6{
t.Fatalf("2*-3 expected be 6, but %d got",Mul(2,-3))}})}
除了使用命令之后,可以使用golang中的如下符号
输出如下所示
/usr/local/Cellar/go/1.19.6/libexec/bin/go tool test2json -t /private/var/folders/cc/wn7xg4yx22d_qp96zw37rrl00000gp/T/___TestMul_in_calculate.test -test.v -test.paniconexit0 -test.run ^\QTestMul\E$
=== RUN TestMul
=== RUN TestMul/pos
=== RUN TestMul/neg
--- PASS: TestMul (0.00s)--- PASS: TestMul/pos (0.00s)--- PASS: TestMul/neg (0.00s)
PASS
Process finished with the exit code 0
📢:之前的例子失败是使用的
t.Error/t.Errorf
,这里使用的是
t.Fatal/Fatalf
,区别在于前者遇到错误不会停止,还会继续执行其他的测试用例,后者遇到❎就会停止。
执行运行其中某个子测试
calculate %go test -run TestMul/pos -v
=== RUN TestMul
=== RUN TestMul/pos
--- PASS: TestMul (0.00s)--- PASS: TestMul/pos (0.00s)
PASS
ok calculate 1.002s
种类的
Run()
第一个参数是不是类似Pytest中的mark.tag标签,但是上面的写法发现冗余别较多,推荐使用如下的方法
funcTestMul(t *testing.T){//if ans := Mul(2,3); ans != 6{// t.Errorf("2*3 expected be 6,but %d got",ans)//}//t.Run("pos", func(t *testing.T) {// if Mul(2, 3) != 6{// t.Fatalf("2*3 expected be 6, but %d got", Mul(2, 3))// }//})//t.Run("neg", func(t *testing.T) {// if Mul(2, -3) != -6{// t.Fatalf("2*-3 expected be 6, but %d got", Mul(2, -3))// }//})
cases :=[]struct{
Name string
A, B, Expected int}{{"pos",2,3,6},{"neg",2,-3,-6},{"zero",0,2,0},}for_, c :=range cases {
t.Run(c.Name,func(t *testing.T){if ans :=Mul(c.A, c.B); ans != c.Expected {
t.Fatalf("%d * %d expected %d, but %d got", c.A, c.B, c.Expected, ans)}})}}
输出如下所示:
calculate %go test -run TestMul -v
=== RUN TestMul
=== RUN TestMul/pos
=== RUN TestMul/neg
=== RUN TestMul/zero
--- PASS: TestMul (0.00s)--- PASS: TestMul/pos (0.00s)--- PASS: TestMul/neg (0.00s)--- PASS: TestMul/zero (0.00s)
PASS
ok calculate 0.343s
上面的用法和pytest中的@pytest.mark.parametrize(‘status’, [‘Pending’, ‘Running’, ‘Success’, ‘Failed’, ‘Timeout’])进行类比,所有用例的测试数据组织在切片cases中,借助于创建子测试,当然目前觉得pytest这样更方便些,go这样写的好处有:
- 新增用例非常简单,只需给 cases 新增一条测试数据即可。
- 测试代码可读性好,直观地能够看到每个子测试的参数和期待的返回值。
- 用例失败时,报错信息的格式比较统一,测试报告易于阅读。
如果数据量较大,或是一些二进制数据,推荐使用相对路径从文件中读取
所以编写测试用例可以抽象总结为以下几点:
- 测试用例文件不会参与正常源码的编译,不会被包含到可执行文件中;
- 测试用例的文件名必须以
_test.go
结尾; - 需要使用
import
导入testing
包; - 测试函数的名称要以Test或Benchmark开头,后面可以跟任意字母组成的字符串,但第一个字母必须大写,例如
TestAbc()
,一个测试用 - 文件中可以包含多个测试函数;
- 单元测试则以(
t *testing.T
)作为参数,性能测试以(t *testing.B
)做为参数; - 测试用例文件使用
go test
命令来执行,源码中不需要main()
函数作为入口,所有以_test.go
结尾的源码文件内以Test
开头的函数都会自动执行。
1.3 帮助函数helpers
我们知道在Pytest中公共的东西可以抽象出来放置在contest.py中,并设置使用级别,如session,function等,对于go中的testing,一些重复的逻辑可以抽出来作为公共的帮助函数helpers,这样的好处无需赘言,增加了测试代码的可维护性和可读性,且使得测试用例的逻辑更加紧凑和清晰,接着上面的示例
# calculate_test.gopackage calculate
import"testing"import"testing"type calcCase struct{
Name string
A, B, Expected int}funcCreateMulTestCase(t *testing.T, c *calcCase,){// t.helpers// if ans := Mul(c.A, c.B); ans != c.Expected {// t.Fatalf("%d * %d expected %d, but %d got", c.A, c.B, c.Expected, ans)// }
t.Helper()
t.Run(c.Name,func(t *testing.T){if ans :=Mul(c.A, c.B); ans != c.Expected {
t.Fatalf("%d * %d expected %d, but %d got", c.A, c.B, c.Expected, ans)}})}funcTestMul(t *testing.T){//if ans := Mul(2,3); ans != 6{// t.Errorf("2*3 expected be 6,but %d got",ans)//}//t.Run("pos", func(t *testing.T) {// if Mul(2, 3) != 6{// t.Fatalf("2*3 expected be 6, but %d got", Mul(2, 3))// }//})//t.Run("neg", func(t *testing.T) {// if Mul(2, -3) != -6{// t.Fatalf("2*-3 expected be 6, but %d got", Mul(2, -3))// }//})//cases := []struct {// Name string// A, B, Expected int//}{// {"pos", 2, 3, 6},// {"neg", 2, -3, -6},// {"zero", 0, 2, -1},//}////for _, c := range cases {// t.Run(c.Name, func(t *testing.T) {// if ans := Mul(c.A, c.B); ans != c.Expected {// t.Fatalf("%d * %d expected %d, but %d got", c.A, c.B, c.Expected, ans)// }// })//}//CreateMulTestCase(t,&calcCase{2,3,6})//CreateMulTestCase(t,&calcCase{2,-3,-6})//CreateMulTestCase(t,&calcCase{0,2,1})CreateMulTestCase(t,&calcCase{"pos",2,3,6})CreateMulTestCase(t,&calcCase{"neg",2,-3,-6})CreateMulTestCase(t,&calcCase{"zero",0,2,1})}
# 这里给出了所以的代码,方便对比查阅,感觉其中演进变化之处
执行结果如下
calculate %go test -run TestMul -v
=== RUN TestMul
calculate_test.go:12:0*2 expected 1, but 0 got
--- FAIL: TestMul (0.00s)
FAIL
exit status 1
FAIL calculate 0.779s
发现有一个失败了,检查一下发现错误❎定位是第12行,我们回溯下
但是这里有三个case都调用了,具体是哪个case有问题还需要一个个排查,这样也太麻烦了吧。因此go 1.9版本中引入了t.Helper(),用于标注该函数是帮助函数,报错时将输出帮助函数调用者的信息,而不是帮助函数内部的信息。检查本机的go版本
calculate %go version
go version go1.19.6 darwin/amd64
然后修改
CreateMulTestCase
,调用
t.Helper()
funcCreateMulTestCase(c *calcCase, t *testing.T){// t.helpers// if ans := Mul(c.A, c.B); ans != c.Expected {// t.Fatalf("%d * %d expected %d, but %d got", c.A, c.B, c.Expected, ans)// }
t.Helper()
t.Run(c.Name,func(t *testing.T){if ans :=Mul(c.A, c.B); ans != c.Expected {
t.Fatalf("%d * %d expected %d, but %d got", c.A, c.B, c.Expected, ans)}})}
输出如下所示:
calculate %go test -run TestMul -v
=== RUN TestMul
=== RUN TestMul/pos
=== RUN TestMul/neg
=== RUN TestMul/zero
calculate_test.go:61:0*2 expected -1, but 0 got
--- FAIL: TestMul (0.00s)--- PASS: TestMul/pos (0.00s)--- PASS: TestMul/neg (0.00s)--- FAIL: TestMul/zero (0.00s)
FAIL
exit status 1
FAIL calculate 0.397s
对于使用t.Helper(),有两点要注意:
- 不要返回错误, 帮助函数内部直接使用 t.Error 或 t.Fatal 即可,在用例主逻辑中不会因为太多的错误处理代码,影响可读性。
- 调用 t.Helper() 让报错信息更准确,有助于定位。
1.4 setup和teardown
一般我们编写自动化测试用例时,非业务检查逻辑会放置前者准备中诸如数据准备,抽象出一部分公共逻辑写在 setup 和 teardown 函数中。例如执行前需要实例化待测试的对象,如果这个对象比较复杂,很适合将这一部分逻辑提取出来;执行后,可能会做一些资源回收类的工作,例如关闭网络连接,释放文件等。标准库 testing 提供了这样的机制。
234567891011121314151617181920212223funcsetup(){
fmt.Println("Before all tests")}functeardown(){
fmt.Println("After all tests")}funcTest1(t *testing.T){
fmt.Println("I'm test1")}funcTest2(t *testing.T){
fmt.Println("I'm test2")}funcTestMain(m *testing.M){setup()
code := m.Run()teardown()
os.Exit(code)}
- 在这个测试文件中,包含有2个测试用例,Test1 和 Test2。
- 如果测试文件中包含函数 TestMain,那么生成的测试将调用 TestMain(m),而不是直接运行测试。
- 调用 m.Run() 触发所有测试用例的执行,并使用 os.Exit() 处理返回的状态码,如果不为0,说明有用例失败。
- 因此可以在调用 m.Run() 前后做一些额外的准备(setup)和回收(teardown)工作。
执行 go test,将会输出
$ go test
Before all tests
I'm test1
I'm test2
PASS
After all tests
ok example 0.006s
2.性能测试
对于性能测试,Go语言标准库内置的testing测试框架提供了基准测试benchmark的能力,可以很容易的对一段代码进行性能测试。性能测试受环境的影响很大,为了保证测试的可重复性,在进行性能测试时,尽可能地保持测试环境的稳定。
- 机器处于闲置状态,测试时不要执行其他任务,也不要和其他人共享硬件资源。
- 机器是否关闭了节能模式,一般笔记本会默认打开这个模式,测试时关闭。
- 避免使用虚拟机和云主机进行测试,一般情况下,为了尽可能地提高资源的利用率,虚拟机和云主机 CPU 和内存一般会超分配,超分机器的性能表现会非常地不稳定。
2.1 入门示例
还是从一个简单的示例开始介绍,使用斐波那契数列,接着上面的
calculate.go
中新增
Fib
funcFib(n int)int{if n ==0|| n ==1{return n
}returnFib(n-2)+Fib(n-1)}
在
calculate_test.go
中实现一个
benchmark
用例,和单元测试一样,输入
test
会快捷联想出来
funcBenchmarkFib(b *testing.B){for i :=0; i < b.N; i++{Fib(30)}}
benchmark
和普通的单元测试用例一样,都位于 _test.go 文件中。- 函数名以
Benchmark
开头,参数是b *testing.B
。和普通的单元测试用例很像,单元测试函数名以Test
开头,参数是t *testing.T
运行示例
go test <module name>/<package name>
用来运行某个
package
内的所有测试用例。
运行当前 package 内的用例:
go test calculate
或
go test .
运行子 package 内的用例:
go test calculate/<package name>
或
go test ./<package name>
如果想递归测试当前目录下的所有的
package:go test ./...
或
go test calculate/...
。
go test
命令默认不运行
benchmark
用例的,如果我们想运行
benchmark
用例,则需要加上
-bench
参数。
calculate %go test -bench .
goos: darwin
goarch: amd64
pkg: calculate
cpu:Intel(R)Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkFib-123273546075 ns/op
PASS
ok calculate 1.887s
-bench 参数支持传入一个正则表达式,匹配到的用例才会得到执行,例如,只运行以
Fib
结尾的
benchmark
calculate %go test -bench="Fib$".
goos: darwin
goarch: amd64
pkg: calculate
cpu:Intel(R)Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkFib-123253589261 ns/op
PASS
ok calculate 2.174s
2.3 benchmark是如何工作的
benchmark
用例的参数
b *testing.B
,有个属性
b.N
表示这个用例需要运行的次数。
b.N
对于每个用例都是不一样的。
那这个值是如何决定的呢?
b.N
从 1 开始,如果该用例能够在 1s 内完成,
b.N
的值便会增加,再次执行。
b.N
的值大概以 1, 2, 3, 5, 10, 20, 30, 50, 100 这样的序列递增,越到后面,增加得越快。我们仔细观察上述例子的输出:
BenchmarkFib-122025980669 ns/op
BenchmarkFib-12
中的 -12 即
GOMAXPROCS
,默认等于 CPU 核数。可以通过
-cpu
参数改变
GOMAXPROCS
,
-cpu
支持传入一个列表作为参数,例如
calculate %go test -bench="Fib$"-cpu=2,4.
goos: darwin
goarch: amd64
pkg: calculate
cpu:Intel(R)Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkFib-23323557771 ns/op
BenchmarkFib-43433512955 ns/op
PASS
ok calculate 3.460s
在这个例子中,改变
-cpu
的核数对结果几乎没有影响,因为这个
Fib
的调用是串行的。
332
和
3557771 ns/op
表示用例执行了
343
次,每次花费约 0.006s。总耗时比 1s 略多
3.3 提升准确度
对于性能测试来说,提升测试准确度是一个重要手段就是增加测试的次数,即所谓的常稳测试,可以使用
-benchtime
和
-count
,其中
-benchtime
默认为1s,可以设置指定
-benchtime
=5s`,如:
calculate %go test -bench="Fib$"-benchtime=5s .
goos: darwin
goarch: amd64
pkg: calculate
cpu:Intel(R)Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkFib-1217123466178 ns/op
PASS
ok calculate 6.855s
实际执行的时间是 6.8s,比 benchtime 的 5s 要长,测试用例编译、执行、销毁等是需要时间的。 `-benchtime` 设置为 5s,用例执行次数也变成了原来的 5倍,每次函数调用时间仍为 0.6s,几乎没有变化。
`-benchtime` 的值除了是时间外,还可以是具体的次数。例如,执行 30 次可以用 `-benchtime=50x`
calculate %go test -bench="Fib$"-benchtime=30x .
goos: darwin
goarch: amd64
pkg: calculate
cpu:Intel(R)Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkFib-12303683029 ns/op
PASS
ok calculate 1.113s
Fib调用了30次,花费1.1s。接着使用-count设置执行benchmark的论数,有点类似pytest中的repeat,例如进行5轮测试
calculate %go test -bench="Fib$"-benchtime=10s -count=5.
goos: darwin
goarch: amd64
pkg: calculate
cpu:Intel(R)Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkFib-1233583493746 ns/op
BenchmarkFib-1233303466845 ns/op
BenchmarkFib-1234723440367 ns/op
BenchmarkFib-1234423431086 ns/op
BenchmarkFib-1233793429828 ns/op
PASS
ok calculate 61.065s
版权归原作者 止语--- 所有, 如有侵权,请联系我们删除。