Go 代码测试(下):深入 Go 单元测试类型及项目测试实战
本文章主要内容引用自:1.孔令飞,Go 语言项目开发实战
这一篇文章将主要介绍Go 语言中的其他测试类型:示例测试、TestMain函数、Mock测试、Fake测试等,并且介绍下IAM项目是如何编写和运行测试用例的。
示例测试
示例测试以 Example
开头,没有输入和返回参数,通常保存在 example_test.go
文件中。示例测试可能包含以 Output:
或者 Unordered output:
开头的注释,这些注释放在函数的结尾部分。 Unordered output:
开头的注释会忽略输出行的顺序。
执行 go test
命令时,会执行这些示例测试,并且go test会将示例测试输出到标准输出的内容,跟注释作对比(比较时将忽略行前后的空格)。如果相等,则示例测试通过测试;如果不相等,则示例测试不通过测试。下面是一个示例测试(位于example_test.go文件中):
1 |
|
执行go test命令,测试 ExampleMax
示例测试:
1 |
|
可以看到 ExampleMax
测试通过。这里测试通过是因为 fmt.Println(Max(1, 2))
向标准输出输出了 2
,跟 // Output:
后面的 2
一致。
当示例测试不包含 Output:
或者 Unordered output:
注释时,执行 go test
只会编译这些函数,但不会执行这些函数。
示例测试命名规范
示例测试需要遵循一些命名规范,因为只有这样,Godoc才能将示例测试和包级别的标识符进行关联。例如,有以下示例测试(位于example_test.go文件中):
1 |
|
Godoc将在 Reverse
函数的文档旁边提供此示例,如下图所示:
示例测试名以 Example
开头,后面可以不跟任何字符串,也可以跟函数名、类型名或者 类型_方法名
,中间用下划线 _
连接,例如:
1 |
|
当某个函数/类型/方法有多个示例测试时,可以通过后缀来区分,后缀必须以小写字母开头,例如:
1 |
|
大型示例
有时候,我们需要编写一个大型的示例测试,这时候我们可以编写一个整文件的示例(whole file example),它有这几个特点:文件名以 _test.go
结尾;只包含一个示例测试,文件中没有单元测试函数和性能测试函数;至少包含一个包级别的声明;当展示这类示例测试时,godoc会直接展示整个文件。例如:
1 |
|
一个包可以包含多个whole file example,一个示例一个文件,例如 example_interface_test.go
、 example_keys_test.go
、 example_search_test.go
等。
TestMain函数
有时候,我们在做测试的时候,可能会在测试之前做些准备工作,例如创建数据库连接等;在测试之后做些清理工作,例如关闭数据库连接、清理测试文件等。这时,我们可以在 _test.go
文件中添加 TestMain
函数,其入参为 *testing.M
。
TestMain
是一个特殊的函数(相当于main函数),测试用例在执行时,会先执行 TestMain
函数,然后可以在 TestMain
中调用 m.Run()
函数执行普通的测试函数。在 m.Run()
函数前面我们可以编写准备逻辑,在 m.Run()
后面我们可以编写清理逻辑。
我们在示例测试文件 math_test.go 中添加如下TestMain函数:
1 |
|
执行go test,输出如下:
1 |
|
在执行测试用例之前,打印了 do some setup
,在测试用例运行完成之后,打印了 do some cleanup
。
IAM项目的测试用例中,使用TestMain函数在执行测试用例前连接了一个fake数据库,代码如下(位于 internal/apiserver/service/v1/user_test.go 文件中):
1 |
|
单元测试、性能测试、示例测试、TestMain函数是go test支持的测试类型。此外,为了测试在函数内使用了Go Interface的函数,我们还延伸出了Mock测试和Fake测试两种测试类型。
Mock测试
一般来说,单元测试中是不允许有外部依赖的,那么也就是说,这些外部依赖都需要被模拟。在Go中,一般会借助各类Mock工具来模拟一些依赖。
GoMock是由Golang官方开发维护的测试框架,实现了较为完整的基于interface的Mock功能,能够与Golang内置的testing包良好集成,也能用于其他的测试环境中。GoMock测试框架包含了GoMock包和mockgen工具两部分,其中GoMock包用来完成对象生命周期的管理,mockgen工具用来生成interface对应的Mock类源文件。下面,我来分别详细介绍下GoMock包和mockgen工具,以及它们的使用方法。
安装GoMock
要使用GoMock,首先需要安装GoMock包和mockgen工具,安装方法如下:
1 |
|
下面,我通过一个 获取当前Golang最新版本的例子,来给你演示下如何使用GoMock。示例代码目录结构如下(目录下的代码见 gomock):
1 |
|
spider.go
文件中定义了一个 Spider
接口, spider.go
代码如下:
1 |
|
Spider
接口中的GetBody方法可以抓取 https://golang.org
首页的 Build version
字段,来获取Golang的最新版本。
我们在 go_version.go
文件中,调用 Spider
接口的 GetBody
方法, go_version.go
代码如下:
1 |
|
GetGoVersion
函数直接返回表示版本的字符串。正常情况下,我们会写出如下的单元测试代码:
1 |
|
上面的测试代码,依赖 spider.CreateGoVersionSpider()
返回一个实现了 Spider
接口的实例(爬虫)。但很多时候, spider.CreateGoVersionSpider()
爬虫可能还没有实现,或者在单元测试环境下不能运行(比如,在单元测试环境中连接数据库),这时候 TestGetGoVersion
测试用例就无法执行。
那么,如何才能在这种情况下运行 TestGetGoVersion
测试用例呢?这时候,我们就可以通过Mock工具,Mock一个爬虫实例。接下来我讲讲具体操作。
首先,用 GoMock 提供的mockgen工具,生成要 Mock 的接口的实现,我们在gomock目录下执行以下命令:
1 |
|
上面的命令会在 spider/mock
目录下生成 mock_spider.go
文件:
1 |
|
mock_spider.go
文件中,定义了一些函数/方法,可以支持我们编写 TestGetGoVersion
测试函数。这时候,我们的单元测试代码如下(见 go_version_test.go 文件):
1 |
|
这一版本的 TestGetGoVersion
通过GoMock, Mock了一个 Spider
接口,而不用去实现一个 Spider
接口。这就大大降低了单元测试用例编写的复杂度。通过Mock,很多不能测试的函数也变得可测试了。
通过上面的测试用例,我们可以看到,GoMock 和 上一讲 介绍的testing单元测试框架可以紧密地结合起来工作。
mockgen工具介绍
上面,我介绍了如何使用 GoMock 编写单元测试用例。其中,我们使用到了 mockgen
工具来生成 Mock代码, mockgen
工具提供了很多有用的功能,这里我来详细介绍下。
mockgen
工具是 GoMock 提供的,用来Mock一个Go接口。它可以根据给定的接口,来自动生成Mock代码。这里,有两种模式可以生成Mock代码,分别是源码模式和反射模式。
- 源码模式
如果有接口文件,则可以通过以下命令来生成Mock代码:
1 |
|
上面的命令,Mock了 spider/spider.go
文件中定义的 Spider
接口,并将Mock代码保存在 spider/mock/mock_spider.go
文件中,文件的包名为 spider
。
mockgen工具的参数说明见下表:
- 反射模式
此外,mockgen工具还支持通过使用反射程序来生成 Mock 代码。它通过传递两个非标志参数,即导入路径和逗号分隔的接口列表来启用,其他参数和源码模式共用,例如:
1 |
|
通过注释使用mockgen
如果有多个文件,并且分散在不同的位置,那么我们要生成Mock文件的时候,需要对每个文件执行多次mockgen命令(这里假设包名不相同)。这种操作还是比较繁琐的,mockgen还提供了一种通过注释生成Mock文件的方式,此时需要借助 go generate
工具。
在接口文件的代码中,添加以下注释(具体代码见 spider.go 文件):
1 |
|
这时候,我们只需要在 gomock
目录下,执行以下命令,就可以自动生成Mock代码:
1 |
|
使用Mock代码编写单元测试用例
生成了Mock代码之后,我们就可以使用它们了。这里我们结合 testing
来编写一个使用了Mock代码的单元测试用例。
首先, 需要在单元测试代码里创建一个Mock控制器:
1 |
|
将 *testing.T
传递给GoMock ,生成一个 Controller
对象,该对象控制了整个Mock的过程。在操作完后,还需要进行回收,所以一般会在 NewController
后面defer一个Finish,代码如下:
1 |
|
然后, 就可以调用Mock的对象了:
1 |
|
这里的 spider
是mockgen命令里面传递的包名,后面是 NewMockXxxx
格式的对象创建函数, Xxx
是接口名。这里,我们需要传递控制器对象进去,返回一个Mock实例。
接着, 有了Mock实例,我们就可以调用其断言方法 EXPECT()
了。
gomock采用了链式调用法,通过 .
连接函数调用,可以像链条一样连接下去。例如:
1 |
|
Mock一个接口的方法,我们需要Mock该方法的入参和返回值。我们可以通过参数匹配来Mock入参,通过Mock实例的 Return
方法来Mock返回值。下面,我们来分别看下如何指定入参和返回值。
先来看如何指定入参。如果函数有参数,我们可以使用参数匹配来指代函数的参数,例如:
1 |
|
gomock支持以下参数匹配:
- gomock.Any(),可以用来表示任意的入参。
- gomock.Eq(value),用来表示与 value 等价的值。
- gomock.Not(value),用来表示非 value 以外的值。
- gomock.Nil(),用来表示 None 值。
接下来,我们看如何指定返回值。
EXPECT()
得到Mock的实例,然后调用Mock实例的方法,该方法返回第一个 Call
对象,然后可以对其进行条件约束,比如使用Mock实例的 Return
方法约束其返回值。 Call
对象还提供了以下方法来约束Mock实例:
1 |
|
上面列出了多个 Call
对象提供的约束方法,接下来我会介绍3个常用的约束方法:指定返回值、指定执行次数和指定执行顺序。
- 指定返回值
我们可以提供调用 Call
的 Return
函数,来指定接口的返回值,例如:
1 |
|
- 指定执行次数
有时候,我们需要指定函数执行多少次,例如:对于接受网络请求的函数,计算其执行了多少次。我们可以通过 Call
的 Times
函数来指定执行次数:
1 |
|
上述代码,执行了三次Recv函数,这里gomock还支持其他的执行次数限制:
- AnyTimes(),表示执行0到多次。
- MaxTimes(n int),表示如果没有设置,最多执行n次。
- MinTimes(n int),表示如果没有设置,最少执行n次。
- 指定执行顺序
有时候,我们还要指定执行顺序,比如要先执行 Init 操作,然后才能执行Recv操作:
1 |
|
最后,我们可以使用 go test
来测试使用了Mock代码的单元测试代码:
1 |
|
Fake测试
在Go项目开发中,对于比较复杂的接口,我们还可以Fake一个接口实现,来进行测试。所谓Fake测试,其实就是针对接口实现一个假(fake)的实例。至于如何实现Fake实例,需要你根据业务自行实现。例如:IAM项目中iam-apiserver组件就实现了一个fake store,代码见 fake 目录。接下来基于IAM项目测试实战部分介绍
IAM项目测试实战
IAM项目是如何运行测试用例的?
首先,我们来看下IAM项目是如何执行测试用例的。
在IAM项目的源码根目录下,可以通过运行 make test
执行测试用例, make test
会执行 iam/scripts/make-rules/golang.mk
文件中的 go.test
伪目标,规则如下:
1 |
|
在上述规则中,我们执行 go test
时设置了超时时间、竞态检查,开启了代码覆盖率检查,覆盖率测试数据保存在了 coverage.out
文件中。在Go项目开发中,并不是所有的包都需要单元测试,所以上面的命令还过滤掉了一些不需要测试的包,这些包配置在 EXCLUDE_TESTS
变量中:
1 |
|
同时,也调用了 go-junit-report
将go test的结果转化成了xml格式的报告文件,该报告文件会被一些CI系统,例如Jenkins拿来解析并展示结果。上述代码也同时生成了coverage.html文件,该文件可以存放在制品库中,供我们后期分析查看。
这里需要注意,Mock的代码是不需要编写测试用例的,为了避免影响项目的单元测试覆盖率,需要将Mock代码的单元测试覆盖率数据从 coverage.out
文件中删除掉, go.test
规则通过以下命令删除这些无用的数据:
1 |
|
另外,还可以通过 make cover
来进行单元测试覆盖率测试, make cover
会执行 iam/scripts/make-rules/golang.mk
文件中的 go.test.cover
伪目标,规则如下:
1 |
|
上述目标依赖 go.test
,也就是说执行单元测试覆盖率目标之前,会先进行单元测试,然后使用单元测试产生的覆盖率数据 coverage.out
计算出总的单元测试覆盖率,这里是通过 coverage.awk 脚本来计算的。
如果单元测试覆盖率不达标,Makefile会报错并退出。可以通过Makefile的 COVERAGE 变量来设置单元测试覆盖率阈值。
COVERAGE的默认值为60,我们也可以在命令行手动指定,例如:
1 |
|
为了确保项目的单元测试覆盖率达标,需要设置单元测试覆盖率质量红线。一般来说,这些红线很难靠开发者的自觉性去保障,所以好的方法是将质量红线加入到CICD流程中。
所以,在 Makefile
文件中,我将 cover
放在 all
目标的依赖中,并且位于build之前,也就是 all: gen add-copyright format lint cover build
。这样每次当我们执行make时,会自动进行代码测试,并计算单元测试覆盖率,如果覆盖率不达标,则停止构建;如果达标,继续进入下一步的构建流程。
IAM项目测试案例分享
- 单元测试案例
我们可以手动编写单元测试代码,也可以使用gotests工具生成单元测试代码。
先来看手动编写测试代码的案例。这里单元测试代码见 Test_Option,代码如下:
1 |
|
上述代码中,使用了 github.com/stretchr/testify/assert
包来对比结果。
再来看使用gotests工具生成单元测试代码的案例(Table-Driven 的测试模式)。出于效率上的考虑,IAM项目的单元测试用例,基本都是使用gotests工具生成测试用例模板代码,并基于这些模板代码填充测试Case的。代码见 service_test.go 文件。
- 性能测试案例
IAM项目的性能测试用例,见 BenchmarkListUser 测试函数。代码如下:
1 |
|
- 示例测试案例
IAM项目的示例测试用例见 example_test.go 文件。 example_test.go
中的一个示例测试代码如下:
1 |
|
- TestMain测试案例
IAM项目的TestMain测试案例,见 user_test.go 文件中的 TestMain
函数:
1 |
|
TestMain
函数初始化了fake Factory,然后调用 m.Run
执行测试用例。
- Mock测试案例
Mock代码见 internal/apiserver/service/v1/mock_service.go,使用Mock的测试用例见 internal/apiserver/controller/v1/user/create_test.go 文件。因为代码比较多,这里建议你打开链接,查看测试用例的具体实现。
我们可以在IAM项目的根目录下执行以下命令,来自动生成所有的Mock文件:
1 |
|
- Fake测试案例
fake store代码实现位于 internal/apiserver/store/fake 目录下。fake store的使用方式,见 user_test.go 文件:
1 |
|
上述代码通过 TestMain
初始化fake实例( store.Factory 接口类型):
1 |
|
GetFakeFactoryOr
函数,创建了一些fake users、secrets、policies,并保存在了 fakeFactory
变量中,供后面的测试用例使用,例如BenchmarkListUser、Test_newUsers等。
其他测试工具/包
测试框架
- Testify框架:Testify是Go test的预判工具,它能让你的测试代码变得更优雅和高效,测试结果也变得更详细。
- GoConvey框架:GoConvey是一款针对Golang的测试框架,可以管理和运行测试用例,同时提供了丰富的断言函数,并支持很多 Web 界面特性。
Mock工具
这篇文章介绍了Go官方提供的Mock框架GoMock,不过还有一些其他的优秀Mock工具可供我们使用。这些Mock工具分别用在不同的Mock场景中
- sqlmock:可以用来模拟数据库连接。数据库是项目中比较常见的依赖,在遇到数据库依赖时都可以用它。
- httpmock:可以用来Mock HTTP请求。
- bouk/monkey:猴子补丁,能够通过替换函数指针的方式来修改任意函数的实现。如果golang/mock、sqlmock和httpmock这几种方法都不能满足我们的需求,我们可以尝试用猴子补丁的方式来Mock依赖。可以这么说,猴子补丁提供了单元测试 Mock 依赖的最终解决方案。
总结
这一篇文章介绍了除单元测试和性能测试之外的另一些测试方法。
除了示例测试和TestMain函数,我还详细介绍了Mock测试,也就是如何使用GoMock来测试一些在单元测试环境下不好实现的接口。绝大部分情况下,可以使用GoMock来Mock接口,但是对于一些业务逻辑比较复杂的接口,我们可以通过Fake一个接口实现,来对代码进行测试,这也称为Fake测试。
除此之外,我们还可以使用其他一些测试框架,例如Testify框架和GoConvey框架。在Go代码测试中,我们最常使用的是Go官方提供的Mock框架GoMock,但仍然有其他优秀的Mock工具,可供我们在不同场景下使用,例如sqlmock、httpmock、bouk/monkey等。