简介
testing是 Go 语言标准库自带的测试库。在 Go 语言中编写测试很简单,只需要遵循 Go 测试的几个约定,与编写正常的 Go 代码没有什么区别。Go 语言中有 3 种类型的测试:单元测试,性能测试,示例测试。下面依次来介绍。
单元测试
单元测试又称为功能性测试,是为了测试函数、模块等代码的逻辑是否正确。接下来我们编写一个库,用于将表示罗马数字的字符串和整数互转。罗马数字是由M/D/C/L/X/V/I这几个字符根据一定的规则组合起来表示一个正整数:
- M=1000,D=500,C=100,L=50,X=10,V=5,I=1;
- 只能表示 1-3999 范围内的整数,不能表示 0 和负数,不能表示 4000 及以上的整数,不能表示分数和小数(当然有其他复杂的规则来表示这些数字,这里暂不考虑);
- 每个整数只有一种表示方式,一般情况下,连写的字符表示对应整数相加,例如
I=1,II=2,III=3。但是,十位字符(I/X/C/M)最多出现 3 次,所以不能用IIII表示 4,需要在V左边添加一个I(即IV)来表示,不能用VIIII表示 9,需要使用IX代替。另外五位字符(V/L/D)不能连续出现 2 次,所以不能出现VV,需要用X代替。
|
|
在 Go 中编写测试很简单,只需要在待测试功能所在文件的同级目录中创建一个以_test.go结尾的文件。在该文件中,我们可以编写一个个测试函数。测试函数名必须是TestXxxx这个形式,而且Xxxx必须以大写字母开头,另外函数带有一个*testing.T类型的参数:
|
|
在测试函数中编写的代码与正常的代码没有什么不同,调用相应的函数,返回结果,判断结果与预期是否一致,如果不一致则调用testing.T的Errorf()输出错误信息。运行测试时,这些错误信息会被收集起来,运行结束后统一输出。
测试编写完成之后,使用go test命令运行测试,输出结果:
|
|
我故意将ToRoman()函数中写错了一行代码,n > pair.Num中>应该为>=,单元测试成功找出了错误。修改之后重新运行测试:
|
|
这次测试都通过了!
我们还可以给go test命令传入-v选项,输出详细的测试信息:
|
|
在运行每个测试函数前,都输出一行=== RUN,运行结束之后输出--- PASS或--- FAIL信息。
表格驱动测试
在上面的例子中,我们实际上只测试了两种情况,0 和 1。按照这种方式将每种情况都写出来就太繁琐了,Go 中流行使用表格的方式将各个测试数据和结果列举出来:
|
|
上面将要测试的每种情况列举出来,然后针对每个整数调用ToRoman()函数,比较返回的罗马数字字符串和错误值是否与预期的相符。后续要添加新的测试用例也很方便。
分组和并行
有时候对同一个函数有不同维度的测试,将这些组合在一起有利于维护。例如上面对ToRoman()函数的测试可以分为非法值,单个罗马字符和普通 3 种情况。
为了分组,我对代码做了一定程度的重构,首先抽象一个toRomanCase结构:
|
|
将所有的测试数据划分到 3 个组中:
|
|
然后为了避免代码重复,抽象一个运行多个toRomanCase的函数:
|
|
为每个分组定义一个测试函数:
|
|
在原来的测试函数中,调用t.Run()运行不同分组的测试函数,t.Run()第一个参数为子测试名,第二个参数为子测试函数:
|
|
运行:
|
|
可以看到,依次运行 3 个子测试,子测试名是父测试名和t.Run()指定的名字组合而成的,如TestToRoman/Invalid。
默认情况下,这些测试都是依次顺序执行的。如果各个测试之间没有联系,我们可以让他们并行以加快测试速度。方法也很简单,在testToRomanInvalid/testToRomanSingle/testToRomanNormal这 3 个函数开始处调用t.Parallel(),由于这 3 个函数直接调用了testToRomanCases,也可以只在testToRomanCases函数开头出添加:
|
|
运行:
|
|
我们发现测试完成的顺序并不是我们指定的顺序。
另外,这个示例中我将roman_test.go文件移到了roman_test包中,所以需要import "github.com/darjun/go-daily-lib/testing/roman"。这种方式在测试包有循环依赖的情况下非常有用,例如标准库中net/http依赖net/url,url的测试函数依赖net/http,如果把测试放在net/url包中,那么就会导致循环依赖url_test(net/url)->net/http->net/url。这时可以将url_test放在一个独立的包中。
主测试函数
有一种特殊的测试函数,函数名为TestMain(),接受一个*testing.M类型的参数。这个函数一般用于在运行所有测试前执行一些初始化逻辑(如创建数据库链接),或所有测试都运行结束之后执行一些清理逻辑(释放数据库链接)。如果测试文件中定义了这个函数,则go test命令会直接运行这个函数,否者go test会创建一个默认的TestMain()函数。这个函数的默认行为就是运行文件中定义的测试。我们自定义TestMain()函数时,也需要手动调用m.Run()方法运行测试函数,否则测试函数不会运行。默认的TestMain()类似下面代码:
|
|
下面自定义一个TestMain()函数,打印go test支持的选项:
|
|
运行:
|
|
这些选项也可以通过go help testflag查看。
其他
另一个函数FromRoman()我没有写任何测试,就交给大家了😀
性能测试
性能测试是为了对函数的运行性能进行评测。性能测试也必须在_test.go文件中编写,且函数名必须是BenchmarkXxxx开头。性能测试函数接受一个*testing.B的参数。下面我们编写 3 个计算第 n 个斐波那契数的函数。
第一种方式:递归
|
|
第二种方式:备忘录
|
|
第三种方式:迭代
|
|
下面我们来测试这 3 个函数的执行效率:
|
|
需要特别注意的是N,go test会一直调整这个数值,直到测试时间能得出可靠的性能数据为止。运行:
|
|
性能测试默认不会执行,需要通过-bench=.指定运行。-bench选项的值是一个简单的模式,.表示匹配所有的,Fib表示运行名字中有Fib的。
上面的测试结果表示Fib1在指定时间内执行了 31110 次,平均每次 39144 ns,Fib2在指定时间内运行了 582637 次,平均每次耗时 3127 ns,Fib3在指定时间内运行了 191600582 次,平均每次耗时 5.588 ns。
其他选项
有一些选项可以控制性能测试的执行。
-benchtime:设置每个测试的运行时间。
|
|
运行了更长的时间:
|
|
-benchmem:输出性能测试函数的内存分配情况。
-memprofile file:将内存分配数据写入文件。
-cpuprofile file:将 CPU 采样数据写入文件,方便使用go tool pprof工具分析,详见我的另一篇文章《你不知道的 Go 之 pprof》
运行:
|
|
同时生成了 CPU 采样数据和内存分配数据,通过go tool pprof分析:
|
|
内存:
|
|
示例测试
示例测试用于演示模块或函数的使用。同样地,示例测试也在文件_test.go中编写,并且示例测试函数名必须是ExampleXxx的形式。在Example*函数中编写代码,然后在注释中编写期望的输出,go test会运行该函数,然后将实际输出与期望的做比较。下面摘取自 Go 源码net/url/example_test.go文件中的代码演示了url.Values的用法:
|
|
注释中Output:后是期望的输出结果,go test会运行这些函数并与期望的结果做比较,比较会忽略空格。
有时候我们输出的顺序是不确定的,这时就需要使用Unordered Output。我们知道url.Values底层类型为map[string][]string,所以可以遍历输出所有的键值,但是输出顺序不确定:
|
|
运行:
|
|
没有注释,或注释中无Output/Unordered Output的函数会被忽略。
总结
本文介绍了 Go 中的 3 种测试:单元测试,性能测试和示例测试。为了让程序更可靠,让以后的重构更安全、更放心,单元测试必不可少。排查程序中的性能问题,性能测试能派上大用场。示例测试主要是为了演示如何使用某个功能。
大家如果发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue😄
参考
- testing 官方文档: https://golang.google.cn/pkg/testing/
- Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib
我
欢迎关注我的微信公众号【GoUpUp】,共同学习,一起进步~
