当前位置:网站首页>Golang单元测试、Mock测试以及基准测试
Golang单元测试、Mock测试以及基准测试
2022-07-03 17:10:00 【小菜鸡本菜】
之前参加字节跳动青训营而整理的笔记
Golang拥有一套单元测试和性能测试系统,仅需要添加很少的代码就可以快速测试一段需求代码。
一、单元测试
单元测试主要包括:输入、测试单元、输出、期望以及与期望的校对。
测试单元包括函数或者结合了一些函数的模块等。我们通过将输出与期望值进行校对,来验证代码的正确性。
通过单元测试,可以一方面保证质量,例如在覆盖率足够的情况下,如果在旧代码中添加了新的代码,通过单元测试可以验证新的代码是否破坏了功能正确性。
另一方面,也提升了效率,例如代码中出现了bug,通过编写单元测试,我们能够在较短的时间内定位或修复问题。
1.1、golang规则
规则1:所有测试文件以_test.go
结尾。
_test.go
程序不会被普通的 Go 编译器编译,所以当放应用部署到生产环境时它们不会被部署;
只有gotest
会编译所有的程序:普通程序和测试程序。
规则2:测试文件中必须导入testing
包,并且函数必须写为func TestXxx(*testing.T)
形式。
例如,某个函数Add
的测试函数为TestAdd
,如下所示:
//main.go
func Add(a, b int) int {
return a + b
}
//main_test.go
func TestAdd(t *testing.T) {
trueOutput := Add(1, 2)
expectOutput := 3
if trueOutput != expectOutput {
t.Errorf("Expected %v do not match actual %v", expectOutput, trueOutput)
}
}
规则3:测试的初始化逻辑放到TestMain
中。
这是一个比较好的用法,TestMain
函数具体信息如下:
func TestMain(m *testing.M){
//测试前:数据装载、配置初始化等前置工作
//...
code := m.Run()
//测试后:释放资源等收尾工作
//...
os.Exit(code)
}
例如:
func TestMain(m *testing.M) {
//测试前
fmt.Println("开始了!")
run := m.Run()
//测试后
fmt.Println("结束了!")
os.Exit(run)
}
func TestAdd(t *testing.T) {
trueOutput := Add(1, 2)
expectOutput := 3
if trueOutput != expectOutput {
t.Errorf("Expected %v do not match actual %v", expectOutput, trueOutput)
}
}
//测试结果
//开始了!
//=== RUN TestAdd
//--- PASS: TestAdd (0.00s)
//PASS
//
//结束了!
1.2、举例&第三方测试包
在该例子中,我们期望HelloTom
函数返回“Tom”
,如果返回的不是“Tom”
则表示测试失败。
很明显,本次测试是失败的。
func HelloTom() string {
return "Jerry"
}
func TestHelloTom(t *testing.T) {
output := HelloTome()
expectOutput := "Tom"
if output != expectOutput {
t.Errorf("Expected %s do not match actual %s", expectOutput, output)
}
}
//测试结果
//=== RUN TestHelloTom
//main_test.go:28: Expected Tom do not match actual Jerry
//--- FAIL: TestHelloTom (0.00s)
在单元测试函数中,经常需要编写判断逻辑,我们可以使用一些开源的测试包来帮助简化代码。
例如使用Testift。使用go get
安装:
go get github.com/stretchr/testify
将上述例子使用Testify
后,代码如下:
func TestHelloTom(t *testing.T) {
output := HelloTom()
assert.Equal(t, "Tom", output)
}
1.3、覆盖率
问题:
- 如何衡量代码是否经过了足够的测试?
- 如何评价项目的测试水准?
- 如何评估项目是否达到了高水准测试等级?
我们需要评估单元测试,于是需要引入了单元测试覆盖率。
覆盖率在一定程度上反应了测试用例的覆盖度,越完备那么代码的正确性越有保证。
例子:
func JudgePassLine(score int16) bool {
if score >= 60 {
return true
}
return false
}
func TestJudgePassLine(t *testing.T) {
isPass := JudgePassLine(70)
expectOutput := true
if expectOutput != isPass {
t.Errorf("Expected %v do not match actual %v", expectOutput, isPass)
}
}
使用命令:
go test judgment_test.go judgment.go --cover
结果:
=== RUN TestJudgePassLine
--- PASS: TestJudgePassLine (0.00s)
PASS
coverage: 40.0% of statements in ./...
如果使用Goland的话,会显示出测试代码的范围。很明显,JudgePassLine
函数的前两行(例子中第2、3行)已经被验证,而return false
并没有被验证。
我们可以再写一个分支的单元测试,来提高覆盖率。
func TestJudgePassLine(t *testing.T) {
isPass := JudgePassLine(70)
expectOutput := true
if expectOutput != isPass {
t.Errorf("Expected %v do not match actual %v", expectOutput, isPass)
}
}
func TestJudgePassLineFail(t *testing.T) {
isPass := JudgePassLine(50)
expectOutput := false
if expectOutput != isPass {
t.Errorf("Expected %v do not match actual %v", expectOutput, isPass)
}
}
//结果
//=== RUN TestJudgePassLine
//--- PASS: TestJudgePassLine (0.00s)
//=== RUN TestJudgePassLineFail
//--- PASS: TestJudgePassLineFail (0.00s)
//PASS
//
//coverage: 60.0% of statements in ./...
从结果可以看出,目前覆盖率已经达到60%了(还有其他函数没有写单元测试)。
当然,在实际项目中,要达到100%的覆盖率是一个可望不可及的目标,一般来说,覆盖率在50%~60%能够认为在一些主流的情况下是没有问题的,但是可能还有有一些异常分支没有覆盖到,对一些例如”提现“等资金类的操作,对覆盖率会要求更高,一般会要求达到覆盖率80%以上。
为了能够提高覆盖率,有一些好的实践:
- 测试分支相互独立、全面覆盖。
- 测试单元粒度足够小,因此要求函数单一职责。
二、Mock测试
2.1、项目中的依赖
在一些复杂项目中,会依赖一些数据库、文件或缓存等,这些属于项目的一个强依赖。
单元测试的主要目标有2个:
- 幂等。幂等指重复运行一个测试的结果与之前是一致的。
- 稳定。指单元测试是能够相互隔离的,单元测试中的函数能在任何时间任何地点独立运行。
如果单元测试中直接调用数据库等外部依赖,那测试是不稳定的,例如:
func ReadFirstLine() string {
open, err := os.Open("log")
defer open.Close()
if err != nil {
return ""
}
scanner := bufio.NewScanner(open)
for scanner.Scan() {
return scanner.Text()
}
return ""
}
func ProcessFirstLine() string {
line := ReadFirstLine()
destLine := strings.ReplaceAll(line, "11", "00")
return destLine
}
//Test
func TestProcessFirstLine(t *testing.T) {
firstLine := ProcessFirstLine()
expectOutput := "line00"
if firstLine != expectOutput {
t.Errorf("Expected %s do not match actual %s", expectOutput, firstLine)
}
}
从这个例子中可以看出,测试依赖于外部文件,假如外部文件被删除或篡改了,那么这个测试就不可运行了。
因此就需要引入mock机制。
2.2、Mock
常用的开源Mock包monkey:https://github.com/bouk/monkey
该包提供了快速Mock函数:
- 为一个函数打桩
- 为一个方法打桩
打桩可以理解为用一个函数A去替换一个函数B,B就是原函数,A就是打桩函数。
例子:
将上述读取文件单元测试代码修改,对ReadFirstLine
打桩测试,使测试不再依赖本地文件。
func TestProcessFirstLine(t *testing.T) {
//mock打桩
monkey.Patch(ReadFirstLine, func() string {
return "line00"
})
defer monkey.Unpatch(ReadFirstLine)
//
firstLine := ProcessFirstLine()
expectOutput := "line00"
if firstLine != expectOutput {
t.Errorf("Expected %s do not match actual %s", expectOutput, firstLine)
}
}
mock在运行时实现,基于go的unsafe包,将内存中函数的地址替换成运行时函数地址。
三、基准测试
go提供了基准测试框架,基准测试是指测试一段程序运行时的性能。
在基准测试中,函数会被调用 N 次(N 是非常大的数,如 N = 1000000),并展示 N 的值和函数执行的平均时间,单位为 ns(纳秒,ns/op)。
- 使用基准测试能够优化代码,当然,这需要对当前代码分析。
例子:
负载均衡例子,随机选择执行服务器
var ServerIndex [10]int
func InitServerIndex() {
for i := 0; i < 10; i++ {
ServerIndex[1] = i + 100
}
}
func Select() int {
return ServerIndex[rand.Intn(10)]
}
//测试
//串行的基准测试
func BenchmarkSelect(b *testing.B) {
InitServerIndex()
b.ResetTimer()
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()
}
})
}
测试结果:
BenchmarkSelect-16 180309266(N) 6.596 ns/op(函数执行的平均时间)
BenchmarkSelectParallel-16 29328594 42.33 ns/op
可以看到在并行状态下,性能较为低下,因为Select利用了rand函数,而rand函数为了保证随机性和并发安全,持有一把全局锁,这样就降低了并发性能。
为了提升这个函数的性能,可以用fastrand。
func BenchmarkFastSelectParallel(b *testing.B) {
InitServerIndex()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
FastSelect()
}
})
}
结果:
BenchmarkFastSelectParallel-16 1000000000 0.5274 ns/op
四、总结
- Golang提供了简单而强大的测试工具,而且根据Golang的规则,也使得开发人员能够一眼就明白某个单元测试对应于哪个函数。
- 使用第三方单元测试工具包能够简化我们的代码。
- 在需要使用到外部依赖的情况下,我们可以利用Mock测试来模拟外部依赖,避免发生不必要的错误。
- 基准测试能够得出一段程序的运行性能,便于开发者进行优化,例如上文给出的“随机选择执行服务器”例子。
边栏推荐
- 2021 ICPC regional competition (Shanghai) g.edge groups (tree DP)
- Kotlin学习快速入门(7)——扩展的妙用
- 一位普通程序员一天工作清单
- 【RT-Thread】nxp rt10xx 设备驱动框架之--Audio搭建和使用
- Talk about several methods of interface optimization
- Execute script unrecognized \r
- C语言按行修改文件
- Define a structure fraction to represent a fraction, which is used to represent fractions such as 2/3 and 5/6
- How to promote cross department project collaboration | community essay solicitation
- 线程池:业务代码最常用也最容易犯错的组件
猜你喜欢
【RT-Thread】nxp rt10xx 设备驱动框架之--hwtimer搭建和使用
Luogu: p1155 [noip2008 improvement group] double stack sorting (bipartite graph, simulation)
29:第三章:开发通行证服务:12:开发【获得用户账户信息,接口】;(使用VO类包装查到的数据,以符合接口对返回数据的要求)(在多处都会用到的逻辑,在Controller中可以把其抽成一个共用方法)
kubernetes资源对象介绍及常用命令(五)-(NFS&PV&PVC)
[RT thread] construction and use of --hwtimer of NXP rt10xx device driver framework
One brush 147-force deduction hot question-4 find the median of two positive arrays (H)
手把手带你入门 API 开发
美团一面:为什么线程崩溃崩溃不会导致 JVM 崩溃
Redis:关于列表List类型数据的操作命令
Kotlin学习快速入门(7)——扩展的妙用
随机推荐
LeetCode 1657. Determine whether the two strings are close
远程办公之如何推进跨部门项目协作 | 社区征文
One brush 147-force deduction hot question-4 find the median of two positive arrays (H)
【JokerのZYNQ7020】DDS_ Compiler。
匯編實例解析--實模式下屏幕顯示
Luogu: p1155 [noip2008 improvement group] double stack sorting (bipartite graph, simulation)
Rsync remote synchronization
新库上线 | CnOpenData中国观鸟记录数据
Vs code plug-in korofileheader
[RT thread] NXP rt10xx device driver framework -- Audio construction and use
Simple configuration of postfix server
C语言字符串练习
跨境电商:外贸企业做海外社媒营销的优势
How to promote cross department project collaboration | community essay solicitation
[combinatorics] recursive equation (characteristic equation and characteristic root | example of characteristic equation | root formula of monadic quadratic equation)
Talk about several methods of interface optimization
New library online | cnopendata complete data of Chinese insurance institution outlets
ucore概述
Network security web penetration technology
27. Input 3 integers and output them in descending order. Pointer method is required.