go语言通过自带的testing框架,可以用来实现单元测试与性能测试,通过go test命令来执行单元测试或性能测试。
go test执行单元测试是以包为单位的,如果没有指定包,则默认使用执行命令时所在的包。遍历包下以*_test.go
结尾的文件,执行以Test
,Benchmark
, Example
开头的测试函数。
单元测试
单元测试用例函数以Test开头,例如TestXxx或者Test_xxx。函数的参数必须是testing.T
,可以调用testing.T
的Error 、Errorf 、FailNow 、Fatal 、FatalIf 方法,来说明测试不通过;调用 Log 、Logf 方法来记录测试信息。
执行go test
命令就会执行所在包下的所有测试函数。在测试过程中还有如下常用的参数:
-v
:显示测试函数的运行细节。-run <regexp>
:指定要执行的测试函数-count N
:指定测试函数指定的次数
单元测试模板
测试一个函数,我们通过会测试多个输入与输出是否正确,测试函数的编写也有了较为通用的模板
func TestFunc(t *testing.T) {
// 开始写单元测试逻辑
type args struct {
// 被测试函数的参数
}
tests := []struct {
name string
args args
want TYPE //args作为参数的时候,被测试函数预期的输出结果
}{
// TODO: Add test cases.
}
for _, tt := range tests {
if got := Func(tt.args.x, tt.args.y); got != tt.want {
t.Errorf("Func(args.x, args.y) = %f, want %v", got, tt.want)
}
}
}
具体的细节可能有所不同,但是一般较为规范的单元测试都符合上面的格式。
在上面的got != tt.want
的比较,我们可以使用github.com/stretchr/testify/assert
包来使得比较更加方便和简单。
单元测试自动化生成
单元测试既然可以有通用的模板,那么当然就可以有工具来帮助我们生成这个模板。比如gotests
这个工具。
执行如下命令安装gotests
$ go get -u github.com/cweill/gotests/...
gotests 命令执行格式为:gotests [options] [PATH] [FILE] ...
。gotests 可以为PATH下的所有 Go 源码文件中的函数生成测试代码,也可以只为某个FILE中的函数生成测试代码。
例如$ gotests -all -w .
命令就会为当前目录下的所有函数生成测试代码。然后只需要在模板的TODO位置出添加具体的case即可。
性能测试
性能测试函数必须以Benchmark开头,例如BenchmarkXxx或者Benchmark_xxx。并且其函数参数必须为b testing.B
,函数内b.N
作为循环测试,其中N会在运行时动态调整,直到性能测试函数能够运行足够长的时间,来进行可靠的计时。
func BenchmarkRandInt(b *testing.B) {
for i := 0; i < b.N; i++ {
RandInt()
}
}
go test
命令默认不会执行性能测试函数,需要通过参数-bench <regexp>
来运行指定的测试函数,go test -bench=".*"
表示执行所有的性能测试函数。
在性能测试中,如果被测试函数执行前需要进行一些耗时的准备操作,那么可以在准备工作完成后重置计时,或者先暂停计时准备工作完成后再启动计时。
func BenchmarkXxx(b *testing.B) {
// 准备工作
b.ResetTimer()
for i := 0; i < b.N; i++ {
//...
}
}
func BenchmarkXxx(b *testing.B) {
b.StopTimer() // 调用该函数停止压力测试的时间计数
// 准备工作
b.StartTimer() // 重新开始时间
for i := 0; i < b.N; i++ {
//...
}
}
性能测试中我们还关注如下的参数
-benchmem
:性能测试中关注的内存指标。如 每次执行分配的内存大小(越小,占用内存越少)以及每次执行内存的分配次数(越小,性能越好)。-benchtime
:指定测试时间和循环执行测试,格式为Nx,例如10s表示10秒,100x表示执行100次。-cpu
:指定 GOMAXPROCS。-timeout
:指定测试函数的超时时间
示例测试
示例测试函数以Example开头。例如ExampleXxx或者Example_xxx。示例测试函数没有输入参数和输出参数,但是在函数的结尾可能会有以Output:
或者Unordered output:
开头的注释,Unordered output:
开头的注释会忽略输出行的顺序。例如
func ExampleMax() {
fmt.Println(Max(1, 2))
// Output:
// 2
}
go test
命令默认也会执行示例测试函数,并且将示例测试函数输出到标准输出的内容与注释的内容进行比较(比较时忽略前后空格),相等则测试通过,不相等则测试失败。
对于大型示例测试,可以一个测试函数一个文件,这样godoc展示这类示例测试的时候会直接展示整个文件。
TestMain函数
func TestMain(m *testing.M) {
fmt.Println("do some setup")
m.Run()
fmt.Println("do some cleanup")
}
TestMain是一个特殊的函数,go test
命令执行测试函数的时候,会先执行TestMain函数,TestMain中调用m.Run()
来执行普通的测试函数。所以TestMain函数,我们通过在m.Run()
之前做一些测试的准备工作,例如创建数据库连接;在m.Run()
做一些测试的清理工作,例如关闭数据库连接,删除测试产生的临时文件。
mock测试
一半来说,数据库中是不允许有外部依赖的,例如数据库连接,这些外部依赖都需要被模拟,在go中,就是借用各种mock工具进行模拟的。
GoMock是golang官方开发的测试框架,实现较为完整的基于interface的Mock功能。接下来我们就了解一下使用GoMock进行mock测试。
GoMock测试框架分为两部分,一是GoMock包,用来管理对象生成周期,另一个就是mockgen工具,用来生成interface对应的mock源文件。
首先,安装gomock包和mockgen工具
$ go get github.com/golang/mock/gomock
$ go install github.com/golang/mock/mockgen
GoMock是基于interface的,所以我们现在有如下的接口Store
,其中只有一个方法GetCount
用于获取记录条数。我们可以为不同的数据库来实现这个接口。
type Store interface {
GetCount() int
}
然后有一个函数调用了这个接口实现的方法
func Count(store Store) int {
return store.GetCount()
}
我们现在要对Count
这个函数进行单元测试,但是现在Store接口既没有实现,单元测试环境中也不应该依赖外部数据库,所以我们要对Store
进行mock来获得一个实例。
对接口进行mock,这就要用到mockgen工具了。当前demo的module名为gomock-demo,目录结构如下:
├── count.go
├── go.mod
├── go.sum
└── store
└── store.go
执行下面的命令为Store接口生成mock实现,命令参数后续进行说明
$ mockgen -destination store/mock/store_mock.go -package store gomock-demo/store Store
执行结束后的目录结构如下:
├── count.go
├── go.mod
├── go.sum
└── store
├── mock
│ └── store_mock.go
└── store.go
我们接下来就可以为Count
编写如下的单元测试了
func TestCount(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockStore := store.NewMockStore(ctrl)
mockStore.EXPECT().GetCount().Return(10)
got := Count(mockStore)
if got != 10 {
t.Error("Count wrong res: ", got)
}
}
通过mock,很多无法测试的函数也可以进行测试了。
mockgen说明
mockgen工具用于为接口生成mock实现。有两种生成方式,一种是带-source
的源码模式,例如
mockgen -destination store/mock/store_mock.go -package store -source store/store.go
还有一种就是利用反射程序的反射模式,通过传递两个非标志参数,即导入路径和逗号分隔的接口列表来启用,其他参数和源码模式共用。前面的示例中使用的就是反射模式
$ mockgen -destination store/mock/store_mock.go -package store gomock-demo/store Store
这里介绍再一下命令行的参数即可
-destination
:生成的mock代码所在路径-package
;mock文件的包名-source
:需要mock的接口文件-imports
:依赖的包-aux_files
:接口不止一个文件时,附件文件-build_flags
:传递给build工具的参数
通常前3个参数就已经够用了。
为了方便,可以在接口文件的代码添加注释
//go:generate mockgen -destination mock/store_mock.go -package store -source store.go
这样我们只需要执行下面的命令,就可以为这个接口文件生成mock代码了
go generate ./...
mock代码编写单元测试
通过mockgen工具生成mock代码之后,我们就可以利用生成的mock代码和gomock包编写单元测试了。
首先,创建Mock控制器来管理整个mock过程,并在完成之后进行回收
ctrl := gomock.NewController(t)
defer ctrl.Finish()
然后创建mock实例
mockStore := store.NewMockStore(ctrl)
获得mock实例之后,要想mock一个接口,就需要mock接口的入参和返回值。返回值通过Return来mock,就像前面的示例一样
mockStore.EXPECT().GetCount().Return(10)
这里的GetCount
没有入参,但是对于有入参的方法,这里需要使用如下的参数匹配对入参进行约束
- gomock.Any(),可以用来表示任意的入参。
- gomock.Eq(value),用来表示与 value 等价的值。
- gomock.Not(value),用来表示非 value 以外的值。
- gomock.Nil(),用来表示 None 值。
mock实例进行EXPECT断言,然后调用方法就可以获得第一个Call对象,并对其进行约束
func (c *Call) After(preReq *Call) *Call // After声明调用在preReq完成后执行
func (c *Call) Times(n int) *Call // 设置调用次数为 n 次
func (c *Call) AnyTimes() *Call // 允许调用次数为 0 次或更多次
func (c *Call) MaxTimes(n int) *Call // 设置最大的调用次数为 n 次
func (c *Call) MinTimes(n int) *Call // 设置最小的调用次数为 n 次
func (c *Call) Do(f interface{}) *Call // 声明在匹配时要运行的操作
func (c *Call) Return(rets ...interface{}) *Call // // 声明模拟函数调用返回的值
func (c *Call) SetArg(n int, value interface{}) *Call // 声明使用指针设置第 n 个参数的值
测试覆盖率
为了避免漏掉为某些函数编写测试用例,或者测试用例不够全面。Go提供了cover工具来统计测试覆盖率。
- 生成测试覆盖率数据
$ go test -coverprofile=coverage.out
会在当前目录下生成coverage.out覆盖率数据文件。
- 分析覆盖率文件
$ go tool cover -func=coverage.out
可以查看各个函数的测试覆盖率。可以据此来为函数编写或完善测试用例。
为了更清晰地展示,还可以生成html格式的分析文件
$ go tool cover -html=coverage.out -o coverage.html
标签:代码,Call,mock,测试,测试函数,go,store
From: https://www.cnblogs.com/smarticen/p/17076767.html