热身
单元测试(模块测试)是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。通常而言,一个单元测试是用于判断某个特定条件(或者场景)下某个特定函数的行为。Golang当然也有自带的测试包testing,使用该包可以进行自动化的单元测试,输出结果验证。
如果之前从没用过golang的单元测试的话,可以输入命令 go help test,看看官方的介绍。
这里只打印一些关键信息:
1 | go复制代码E:\mygolandproject\MyTest>go help test |
再执行 go help testfunc 看看
1 | go复制代码E:\mygolandproject\MyTest1>go help testfunc |
现在应该清楚了,要编写一个测试套件,首先需要创建一个名称以 _test.go 结尾的文件,该文件包含 TestXxx 函数:
1 | go复制代码func TestXxx(*testing.T) // Xxx 可以是任何字母数字字符串,但是第一个字母不能是小写字母。 |
go test的基本格式是:
1 | go复制代码go test [build/test flags] [packages] [build/test flags & test binary flags] |
执行 go test 命令后,就会在指定的包下寻找 *_test.go
文件中的 TestXxx 函数来执行。
除了一些可选的 flags 外,需要注意一下 packages 的填写。该*_test.go
测试文件必须要与待测试的文件置于同一包下,执行 go test
或 go test .
或 go test ./xxx_test.go
都可以运行测试套。测试文件不会参与正常源码编译,不会被包含到可执行文件中。
go test
命令会忽略 testdata
目录,该目录是用来保存测试需要用到的辅助数据。
执行完成后就会打印结果信息:
1 | go复制代码ok archive/tar 0.011s |
单元测试
要测试的代码:
1 | go复制代码func Fib(n int) int { |
测试代码:
1 | go复制代码func TestFib(t *testing.T) { |
执行结果如下:
1 | go复制代码E:\myGolandProject\MyTest>go test |
把 expected 改为14,执行结果如下:
1 | go复制代码E:\myGolandProject\MyTest>go test |
测试讲究 case 覆盖,按上面的方式,当我们要覆盖更多 case 时,显然通过修改代码的方式很笨拙。这时我们可以采用 Table-Driven 的方式写测试,标准库中有很多测试是使用这种方式写的。
1 | go复制代码func TestFib(t *testing.T) { |
上面例子中,即使其中某个 case 失败,也不会终止测试执行。
不过可能有小伙伴会觉得为了测试一个简单的函数就要写这么长一段代码,太麻烦了吧!
不用担心,Goland已经具备了一键生成单元测试代码的功能。
如图所示,光标置于函数名之上,右键选择Generate,我们可以选择生成整个package、当前file或者当前选中函数的测试代码。以当前选中函数为例,Goland会自动在当前目录下生成测试文件,内容如下:
1 | go复制代码func TestFib(t *testing.T) { |
我们只需要把测试用例添加到TODO中即可。
这里有个坑需要注意一下,假设原文件是Fib.go
,生成的测试文件是Fib_test.go
。如果我们直接构造测试用例,然后运行go test ./Fib_test.go
的话会报如下错误:
1 | go复制代码# command-line-arguments [command-line-arguments.test] |
解决方法:测试单个文件,需要要带上被测试的原文件,如果原文件有其他引用,也需一并带上。
将go test ./Fib_test.go
改为go test ./Fib.go ./Fib_test.go
即可
继续探索
到这里已经基本介绍了 Golang单元测试的基本流程。但是还有个疑问没解开,就是*testing.T
函数TestFib(t *testing.T)
中的入参 *testing.T
是个啥东西?我们进去源码瞧瞧
1 | go复制代码// T is a type passed to Test functions to manage test state and support formatted test logs. |
可以看到,T是传递给Test函数的类型,用于管理测试状态并支持格式化的测试日志。测试日志会在执行测试的过程中不断累积,并在测试完成时转储至标准输出。
当测试函数返回时,或者当测试函数调用 FailNow
、 Fatal
、Fatalf
、SkipNow
、Skip
、Skipf
中的任意一个时,则宣告该测试函数结束。跟 Parallel
方法一样,以上提到的这些方法只能在运行测试函数的 goroutine 中调用。
至于其他报告方法,比如 Log
以及 Error
的变种, 则可以在多个 goroutine 中同时进行调用。
T 类型内嵌了 common 类型,common 提供这一系列方法,我们经常会用到的(注意,这里说的测试中断,都是指当前测试函数,并不是中断整个测试文件的执行):
- 当我们遇到一个断言错误的时候,标识这个测试失败,会使用到:
1 | yaml复制代码Fail : 测试失败,测试继续,也就是之后的代码依然会执行 |
在 FailNow
方法实现的内部,是通过调用 runtime.Goexit()
来中断测试的。
- 当我们遇到一个断言错误,只希望跳过这个错误并中断,但是不希望标识测试失败,会使用到:
1 | yaml复制代码SkipNow : 跳过测试,测试中断 |
在 SkipNow
方法实现的内部,是通过调用 runtime.Goexit()
来中断测试函数的。
- 当我们只希望打印信息,会用到 :
1 | yaml复制代码Log : 输出信息 |
注意:默认情况下,单元测试成功时,它们打印的信息不会输出,可以通过加上 -v
选项,输出这些信息。但对于基准测试,它们总是会被输出。
- 当我们希望跳过这个测试函数,并且打印出信息,会用到:
1 | yaml复制代码Skip : 相当于 Log + SkipNow |
- 当我们希望断言失败的时候,标识测试失败,并打印出必要的信息,但是测试函数继续执行,会用到:
1 | yaml复制代码Error : 相当于 Log + Fail |
- 当我们希望断言失败的时候,标识测试失败,打印出必要的信息,但中断测试函数,会用到:
1 | yaml复制代码Fatal : 相当于 Log + FailNow |
接着来看一下runtime.Goexit()
的定义:
1 | go复制代码// Goexit terminates the goroutine that calls it. No other goroutine is affected. |
函数头第一句注释就说明了Goexit会终止调用它的goroutine。那问题来了,当某个测试函数断言失败调用FailNow的时候,为什么后面的测试代码还可以执行呢?难道不是一个Goroutine执行完整个测试文件吗?(菜鸡的我刚开始确实是这么想的..)。其实答案就在testing包!
testing包中有一个Runtest函数:
1 | go复制代码// RunTests is an internal function but exported because it is cross-package; |
- 原来Runtest函数就是go test命令的实现!
tests []InternalTest
这个切片入参就是保存着测试文件中所有的测试函数- 调用了runTests,tests切片入参也被传了进去
再看看runTests函数内部实现,我把其他的实现细节屏蔽了:
1 | go复制代码func runTests(matchString func(pat, str string) (bool, error), tests []InternalTest, deadline time.Time) (ran, ok bool) { |
果然是这样,遍历了tests切片,对每个测试函数都调用了Run这个方法
1 | go复制代码// Run runs f as a subtest of t called name. It runs f in a separate goroutine |
答案就在这里,对于每个f,也就是测试函数,都起了一个新的Goroutine来执行!所以当某个测试函数断言失败调用FailNow的时候,后面的测试代码是可以执行的,因为每个TestXxx函数跑在不同的Goroutine上。
扩展
在Go1.17中,给go test
新增了一个-shuffle
选项,shuffle是洗牌的意思,顾名思义就是TestXxx测试方法的执行顺序被打乱了。
切换到Go1.17,执行go help testflag
,找到-shuffle
的描述
1 | go复制代码// ...... |
-shuffle默认是off,设置为on就会打开洗牌。
写个简单Demo验证一下:
1 | go复制代码import ( |
执行结果如下:
1 | go复制代码E:\myGolandProject\MyTest>go test -v -shuffle=on . |
如果按照某种测试顺序会导致错误的话,那么这种错误是很难定位的,这时候就可以利用-shuffle选项来解决这种问题
参考:www.cnblogs.com/Detector/p/…
本文转载自: 掘金