本文在实习期间完成并完善,无任何公司机密,仅做语言交流学习之用。
持续更新。
1.Golang的单元测试
Go语言提供了丰富的单测功能。在Go中,我们通常认为函数是最小的可执行单元。本例中使用两个简单的函数:IsOdd和IsPalindrome来进行Go单测的研究。
在VSCode中,在函数名上点击右键,选择“Go: Generate Unit Tests For Function"即可生成单测文件。
前往对应的*_test.go,开始以表格化的方式填写测试用例。这里我们每个函数都填5个测试用例:
这里name代表的是测试用例的名字,这里建议每个测试用例的名字都唯一,否则你很有可能不知道发生错误的用例到底是哪个。同时我在34行和38行加入了当测试用例不通过时,输出测试用例名字的语句。这样就可以快速定位到测试不通过的测试用例。这里我们故意将第5个测试用例的want写错(32767是奇数,所以本应该返回true的,也就是want应该为true),看看测试工具是怎么报错的。
点击函数名上面那个run test,,这样可以开始执行测试。run test只会运行它所下面一行所声明的测试函数。如果需要测试所有函数可以命令行输入go test或者go test -v。对IsOdd的测试如下:
可以看到其实不自己添加用例输出语句,FAIL的时候go的测试工具也会帮你输出到底是哪个用例不通过。所以34行和38行的代码并不是必须的。我们把第五个测试用例修改成正确的,再次运行测试:
这个时候全部的测试用例就通过了,而且因为这是一种完全只是关心输入输出的测试,并不涉及到函数内部的具体细节,我们称之为“黑盒测试”。
而其中我们还能选择不同的日志等级输出错误信息,单测框架提供的所有日志方法都会结束测试,若只想标记测试用例不通过,请使用t.Fail()。具体的日志方法如表:
方法功能
t.Log()/t.Logf() 打印标准等级日志,同时结束测试
t.Error()/t.Errorf() 打印错误等级日志,同时结束测试
t.Fatal()/t.Fatalf() 打印致命等级日志,同时结束测试
2.Golang的测试覆盖率
Go的测试覆盖率一般指的是测试用例可以触发函数内的多少个分支语句占全部的分支语句的比例,在VSCode中可以以颜色区分的方式来判断当前的测试函数覆盖到了哪些分支,没有覆盖到哪些分支。首先,在测试文件中,右键任意一个测试函数,选择"Go: Toggle Test Coverage In Current Package"来开始进行测试样例覆盖。
这里我们将返回值应为false的测试用例给去掉,执行测试。测试完成后回到被测试的函数源文件中,可以发现被测到的分支为以墨绿色标记,而没有被测试到的代码分支以红色标记。
将测试样例复原后再进行测试,可以发现测试覆盖率达到了100%,同时所有的代码都是墨绿色的了:
3.Golang的基准性能测试
3.1 非并行Golang benchmark
Golang提供了测试函数运行性能的工具,对于所有的函数来说,其性能测试函数都是在前面加Benchmark。我们还是用上面所说的两个函数,来写一下它们的benchmark测试函数(但是我没找到怎么一键生成benchmark的选项):
其中b.ReportAllocs()会报告这个函数的内存使用情况(执行一次方法要申请多少次内存,每次申请需要申请多大的内存),也可以通过指定-benchmem参数来输出所有函数的内存性能。
执行测试命令(或者使用VSCode的那个run benchmark按钮测试单个函数的benchmark):
Linux:
go test -bench=.
go test -bench=. -benchmem # If memory analysis info is required
go test -bench=. -benchtime=3s # If benchmark test time is not 1s, use -benchtime to set it
Windows:
go test -bench="."
go test -bench="." -benchmem # If memory analysis info is required
go test -bench="." -benchtime=3s # If benchmark test time is not 1s, use -benchtime to set it
测试出来的结果如下:
输出解读:
数据意义
BenchmarkIsOdd-8 以P=8来测试IsOdd的性能
232279942 代表在1s内(如果没有指定-benchtime则默认测试时间为1s)执行了IsOdd 232279942次
5.16 ns/op 代表每执行一次IsOdd所花费的时间为5.16 ns
0B/op 代表每执行一次IsOdd所分配的内存为0B
0 allocs/op 代表每执行一次IsOdd申请分配内存的次数为0次
我们新写一个函数,这个函数涉及到分配内存。我们先写一个AllocFixedArray来申请一个长度固定的数组并循环往里面填写数据,然后再写一个AllocMutableArray来申请一个长度可扩充的数组,使这两个申请数组的长度相同,观察它们的性能:
首先对它们做单测,确保代码运行上没有问题,由于这里没有逻辑判断,所以有一个测试用例就够了:
确认结果正确后,写出它们对应的Benchmark函数:
然后可以点击run benchmark一个个测,或者直接全部函数都测一下,这里选择全部测试:
可以发现,使用make声明固定长度的内存是没有allocs的,而append底部会在数组长度不足的时候对数组进行扩充,所以会有内存的申请。并且我们可以看到,append因为底层申请了内存,性能大大下降,AllocMutableArray的执行时间是AllocFixedArray的差不多25倍。这个也提示我们,尽量要对数组的大小有一个预先的估计,并申请好一个capacity比较接近最大上限的数组。
3.2 并行Golang benchmark
测试的时候同样可以使用并行的方法去并发测试指定的时间内能执行多少次该方法,其基本语法为:
b
我们试试将执行比较慢的AllocMutableArray()来并发处理,看看会如何:
执行基准测试,得到结果:
我们可以发现,在P=8并发执行AllocMutableArray之后,执行时间从73.6ns/op降到了14.6ns/op。
3.3 Golang benckmark中的计时器
假设说一个函数在执行之前,要先执行一些外部的初始化操作。而我们如果在go test里面制定了-benchtime选项,它记录的将会是整个Benchmark函数的运行时间。所以我们需要有一种操纵定时器的方法,来获得整个服务精确的运行时间。假设我们的IsOdd,它在执行之前需要睡眠100毫秒,那么我们就可以在执行完睡眠之后,使用重置计数器的方法开始计时。
OK,测试用的总共时间为3.166s。然后我们加上不对初始化进行计时的代码(取消16、18行的注释),重新测一次:
可以看到测试时间有显著的下降,这说明使用b.StopTimer()后,没有将初始化的时间算在总测试时间内。
方法功能
b.StartTimer() 复原或打开计时器,当Benchmark执行前会首先执行b.StartTimer()
b.StopTimer() 暂停当前计时器
b.ResetTimer() 重置当前计时器的值,go官方说该函数在计时器运行时无效,但我试了一下是有效的。建议先b.StopTimer()后再调用此方法,最后再b.StartTimer()
StartTimer, StopTimer和ResetTimer其实就相当于我们常用的秒表的三个按钮:开始,暂停和复位。当Benchmark函数执行之前,就会自动调用StartTimer。而ResetTimer函数生效的前提是必须先调用StopTimer。通过这样的控制,就可以控制基准测试的计时器,防止一些无关部分的时间被测算进来了。
3.4 Golang benchmark的Profile(性能分析)
golang的benchmark提供了一种输出性能分析的工具,在测试benchmark命令的前提下,加上参数即可,下面提供了三种获取全部基准测试函数不同性能的指令:
test -bench
当获得这些性能文件之后,也会相应地留下一个***.test为文件名的可执行文件。为什么要留下这个测试时候生成的临时程序呢?在生成profile文件的时候,为了减少冗余,生成的文件全部都是不含符号信息的,也就是说其实并没有记录性能条目对应的是哪个函数的性能,所以需要有一个这样的副本程序来记录符号信息。
当我们获得这些文件之后,使用go自带的pprof来查看这些文件所表示的含义,其中-nodecount=10表示仅显示前10个最耗性能的条目:
=
其中***为根据实际需要所替换的字符串,一个名为go-learning的程序的cpu占用情况分析如下图:
可以看到,IsPalindrome的占用时间排第2位,仅次于gc。所以我们可以着手去从这个函数进行优化。
4.Golang的Example测试(样例测试)
样例测试比较像平时在一些算法刷题平台(比如LeetCode)的题目的一些例子,样例测试以Example打头,其逻辑也很简单,就是使用fmt.Println输出该测试用例的返回结果,然后在函数体的末尾使用如图的注释,一一对应每个fmt.Println的输出:
17行的output首字母大写小写均可。
如果13~16行输出的结果和18~21行的结果相对应,go test就会PASS,否则就会FAIL,并打印出实际输出和期望输出。
5.Go的Mock方法
5.1 Mock的简介
mock,中文译名为“模仿,假的”,顾名思义就是构建一个模拟对象,来替换掉一些需要在特定环境下触发的服务,使其可以在不修改原服务的前提下达到测试的目的。本文介绍一种是基于gomonkey的函数/变量Mock方法。
5.2 基于gomonkey的函数Mock方法
在使用gomonkey之前,我们要先安装它,输入命令:
go get github.com/agiledragon/gomonkey
并在开头import该包:
import
假设我们有一个函数IsRest,当调用这个函数的时候,程序会判断一下现在的时间是否已经是下午5点之后,如果是,就返回nil,表示现在是下班时间了。否则返回非nil值,表示现在还没到下班时间。我们先写出这个函数:
那我们测试的时候肯定不可能等到5点再去测这个函数吧?否则这测试不就没法做了。这个时候我们先生成它的单测函数,然后施加mock:
其中,108行~114行是对IsRest进行mock的方法,ApplyFunc指的是对函数进行Mock,第二个参数就是要使用的Mock方法。
那我们来执行测试:
说明在执行测试用例的时候,gomonkey成功地把IsRest方法给mock掉了。
6. 总结
Go语言本身提供了丰富的单元测试和性能测试方法,但是在提供Mock方法上还是略有不足。本文从Gomock, Gomonkey和GoStub出发,总结了一些创建Mock对象的方法。如果对于Go测试有进一步兴趣的,可以去了解GoConvey,GoMonkey,GoStub和GoMock的教程,下面列出了一个作者写的关于这四个测试工具的文章,供读者参考:
GoConvey框架使用指南
https://www.jianshu.com/p/633b55d73dddwww.jianshu.comGoMock框架使用指南
https://www.jianshu.com/p/f4e773a1b11fwww.jianshu.comGoStub框架使用指南
https://www.jianshu.com/p/70a93a9ed186www.jianshu.comMonkey框架使用指南
Monkey框架使用指南www.jianshu.com