mock测试不但可以支持io类型的测试,比如:数据库,网络API请求,文件访问等。mock测试还可以做为未开发服务的模拟、服务压力测试支持、对未知复杂的服务进行模拟,比如开发阶段我们依赖的服务还没有开发好,那么就可以使用mock方法来模拟一个服务,模拟的这个服务接收的参数和返回的参数和规划设计的服务是一致的,那我们就可以直接使用这个模拟的服务来协助开发测试了;再比如要对服务进行压力测试,这个时候我们就要把服务依赖的网络,数据等服务进行模拟,不然得到的结果不纯粹。总结一下,有以下几种情况下使用mock会比较好:
- IO类型的,本地文件,数据库,网络API,RPC等
- 依赖的服务还没有开发好,这时候我们自己可以模拟一个服务,加快开发进度提升开发效率
- 压力性能测试的时候屏蔽外部依赖,专注测试本模块
- 依赖的内部函数非常复杂,要构造数据非常不方便,这也是一种
mock测试,简单来说就是通过对服务或者函数发送设计好的参数,并且通过构造注入期望返回的数据来方便以上几种测试开发。
一般情况自己写mock服务是比较费事的事情,而且如果风格不统一,那么后期的管理维护将是软件开发的一个巨大坑,是开发给自己挖的一个坑。所以在就有了很多mock测试框架的出现,框架的出现首先提升了编写mock测试服务的效率,而且编写风格得到了比较好的统一。c/c++也有很多mock框架,Google Mock就是一个比较经典了,java也有很多mock框架,这里就不列举了,今天我们要介绍的是针对golang的mock测试框架。
GoMock是由Golang官方开发维护的测试框架,实现了较为完整的基于interface的Mock功能,能够与Golang内置的testing包良好集成,也能用于其它的测试环境中。GoMock测试框架包含了GoMock包和mockgen工具两部分,其中GoMock包完成对桩对象生命周期的管理,mockgen工具用来生成interface对应的Mock类源文件。
安装
GoMock官网:
https://github.com/golang/mock
GoMock安装:
go get github.com/golang/mock/gomock
mockgen代码生成工具安装:
go get github.com/golang/mock/mockgen
安装好之后,在$GOPATH/src目录下有了github.com/golang/mock子目录,且在该子目录下有GoMock包和mockgen工具。
cd $GOPATH/src/github.com/golang/mock/mockgen
go build
编译后在这个目录下会生成了一个可执行程序mockgen。将mockgen程序移动到$PATH可以找到的目录中: 下面我是在window下的路径,使用了git的shell环境,可以直接看PATH,找到合适的或者新加入进去都ok。
echo $PATH
.....
cp mockgen.exe C:\Users\helightxu\go\bin\
安装之后就可以在命令行直接运行了:mockgen
$ mockgen
mockgen has two modes of operation: source and reflect.
Source mode generates mock interfaces from a source file.
It is enabled by using the -source flag. Other flags that
may be useful in this mode are -imports and -aux_files.
Example:
mockgen -source=foo.go [other options]
......
GoMock文档:
GoMock框架安装完成后,可以使用go doc命令来获取文档:
go doc github.com/golang/mock/gomock
这个文件比较简短,但给出了核心的使用说明。 在GoPkgDoc上也有一个网页版的文档
使用方式介绍
mockgen模式介绍
mockgen有两种操作模式:source和reflect。
Source模式下会从源文件产生mock的interfaces文件。 使用-source参数即可。和这个模式配套使用的参数常有-imports和-aux_files。
mockgen -source=foo.go [other options]
Reflect模式是通过反射的方式来生成mock interfaces。它只需要两个非标志性参数:import路径和需要mock的interface列表,列表使用逗号分割。
例如:
mockgen database/sql/driver Conn,Driver
基本参数介绍
mockgen命令可以把一个包含要Mock的interface的源文件生成一个mock类的源文件。mockgen支持的参数有以下几种:
- -source: 需要mock的文件,这个文件中有需要mock的接口
- -destination: 生成mock代码的文件名。如果你没有设置,生成的代码会被打印到标准输出
- -package: 指定生成的mock文件的包名。如果你没有设置,则包名由mock_和输入文件的包名级拼接而成
- -imports: 生成代码中需要import的包名,形式如foo=bar/baz,并且用逗号分隔。bar/baz是要import的包,foo这个生成的源文件中包的标识。
- -aux_files: 参看附加的文件列表是为了解析类似嵌套的定义在不同文件中的interface。指定元素列表以逗号分隔,元素形式为* foo=bar/baz.go,其中bar/baz.go是源文件,foo是-source选项指定的源文件用到的包名
- -build_flags: 这个参数只在reflect模式下使用,用于go build的时候使用
- -imports: 依赖的需要import的包
- -mock_names:自定义生成mock文件的列表,使用逗号分割。如Repository=MockSensorRepository,Endpoint=MockSensorEndpoint。 Repository、Endpoint为接口,MockSensorRepository,MockSensorEndpoint为相应的mock文件。
在简单的场景下,你将只需使用-source选项。在复杂的情况下,比如一个文件定义了多个interface而你只想对部分interface进行mock,或者interface存在嵌套,这时你需要用反射模式。由于 -destination 选项输入太长,笔者一般不使用该标识符,而使用重定向符号 >,并且mock类代码的输出文件的路径必须是绝对路径。
想了解更多的指令符,可参见官方文档
mockgen工作模式适用场景
mockgen工作模式适用场景如下:
- 对于简单场景,只需使用-source选项。
- 对于复杂场景,如一个源文件定义了多个interface而只想对部分interface进行mock,或者interface存在嵌套,则需要使用反射模式。
测试示例
目录结构
D:\CODE_DEV\SRC\GOMOCKDEMO
│ student.go
│ student_test.go
│
└─mock
mock_people.go
定义一个接口
我们先定义一个打算mock的接口Repository: ```go package gomockdemotype
Ipeople interface
{
GetName() string
SetName(string) string
}
func GetPeopleName(mi Ipeople) string
{
mi.GetName()
return mi.GetName()
}
func SetPeopleName(mi Ipeople, name string) string
{
return mi.SetName(name)
}
<a name="r9cm7"></a>
## 生成mock类文件
```go
$ mockgen gomockdemo Ipeople > mock/mock_people.go
这里需要注意几点:
- mock_people.go文件的mock文件夹,必须先创建好,否则会失败
go_mock一定在$GOPATH/src/的目录下 生成后的文件如下:
// Code generated by MockGen. DO NOT EDIT.// Source: gomockdemo (interfaces: Ipeople)// Package mock_gomockdemo is a generated GoMock package.package mock_gomockdemoimport (
gomock "github.com/golang/mock/gomock"
reflect "reflect")// MockIpeople is a mock of Ipeople interfacetype MockIpeople struct {
ctrl *gomock.Controller
recorder *MockIpeopleMockRecorder
}// MockIpeopleMockRecorder is the mock recorder for MockIpeopletype MockIpeopleMockRecorder struct {
mock *MockIpeople
}// NewMockIpeople creates a new mock instancefunc NewMockIpeople(ctrl *gomock.Controller) *MockIpeople { mock := &MockIpeople{ctrl: ctrl}
mock.recorder = &MockIpeopleMockRecorder{mock} return mock
}// EXPECT returns an object that allows the caller to indicate expected usefunc (m *MockIpeople) EXPECT() *MockIpeopleMockRecorder { return m.recorder}// GetName mocks base methodfunc (m *MockIpeople) GetName() string {
m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetName") ret0, _ := ret[0].(string) return ret0
}// GetName indicates an expected call of GetNamefunc (mr *MockIpeopleMockRecorder) GetName() *gomock.Call {
mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetName", reflect.TypeOf((*MockIpeople)(nil).GetName))
}// SetName mocks base methodfunc (m *MockIpeople) SetName(arg0 string) string {
m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SetName", arg0) ret0, _ := ret[0].(string) return ret0
}// SetName indicates an expected call of SetNamefunc (mr *MockIpeopleMockRecorder) SetName(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetName", reflect.TypeOf((*MockIpeople)(nil).SetName), arg0)
}
测试用例
编写测试用例有一些基本原则,我们一起回顾一下:
每个测试用例只关注一个问题,不要写大而全的测试用例
- 测试用例是黑盒的
- 测试用例之间彼此独立,每个用例要保证自己的前置和后置完备
测试用例要对产品代码非入侵
package gomockdemoimport ( "fmt"
"testing"
"github.com/golang/mock/gomock"
"gomockdemo/mock")func TestGetPeopleName(t *testing.T) { mockCtl := gomock.NewController(t) defer mockCtl.Finish() // 构造mock类, mock_gomockdemo就是生成的mock代码,以包的形式存在
mockpeople := mock_gomockdemo.NewMockIpeople(mockCtl)
//注入期望的返回值
mockpeople.EXPECT().GetName().Return("helight")
mockpeople.EXPECT().GetName().Return("helight")
mockedName := GetPeopleName(mockpeople) if "helight" != mockedName {
t.Error("Get wrong name1: ", mockedName)
} //指定输入参数,返回指定结果
mockpeople.EXPECT().SetName(gomock.Eq("he")).Return("ok") //输出参不做指定,但是指定返回结果
mockpeople.EXPECT().SetName(gomock.Any()).Do(func(format string) {
fmt.Println("recv param2 :", format)
}).Return("ok1") mockedSetName := SetPeopleName(mockpeople,"he")
fmt.Println("mockedSetName: ", mockedSetName) if "ok" != mockedSetName{
t.Error("Set wrong name2: ", mockedSetName)
}
mockedSetName = SetPeopleName(mockpeople,"al222")
fmt.Println("mockedSetName: ", mockedSetName) if "ok1" != mockedSetName{
t.Error("Set wrong name2: ", mockedSetName)
}
}
- gomock.NewController:返回gomock.Controller,它代表 mock 生态系统中的顶级控件。定义了 mock 对象的范围、生命周期和* 期待值。另外它在多个 goroutine 中是安全的
- mockpeople := mock_gomockdemo.NewMockIpeople(mockCtl) // 构造mock实例, mock_gomockdemo就是生成的mock代码,以包的形式存在
- defer mockCtl.Finish() 关闭mock测试
- mockpeople.EXPECT().GetName().Return(“helight”) :EXPECT()是期望拿到返回值,调用的方法是GetName,设定的返回值是“helight”,调用函数也可以指定参数,比如下面的mockpeople.EXPECT().SetName(gomock.Eq(“he”)).Return(“ok”)
- mockedSetName := SetPeopleName(mockpeople,”he”),这里SetPeopleName函数是调用people类的函数,这时候我们传递mockpeople给SetPeopleName就可以测试了,SetPeopleName内部调用的mockpeople和调用真实的people的方式是一模一样的。
测试结果
到此,我们的mock测试就算是ok了,可以再增加一些测试用例和测试值。$ go test -v
=== RUN TestGetPeopleName
gomock.Any: is anything
gomock.Eq: is equal to he
gomock.Any: is anything
mockedSetName: ok
recv param2 : al222
mockedSetName: ok1
--- PASS: TestGetPeopleName (0.00s)
PASS
ok gomockdemo 0.485s
测试覆盖率
这里在介绍一下另外一个简单的测试功能,测试覆盖率的测试cover,只要在go test后面加上-cover就可以了,如下面的例子,这里还加了一个参数-coverprofile=cover.out,这个参数是把覆盖率测试数据导出到cover.out这个文件,然后我们可以使用图形化的方式来看具体的测试覆盖情况。
运行下面这个工具就可以直接把覆盖率以网页的形式打开来看了。$ go test -v -cover -coverprofile=cover.out
=== RUN TestGetPeopleName
gomock.Any: is anything
gomock.Eq: is equal to he
gomock.Any: is anything
mockedSetName: ok
recv param2 : al222
mockedSetName: ok1
--- PASS: TestGetPeopleName (0.00s)
PASS
coverage: 100.0% of statements
ok gomockdemo 0.403s
go tool cover -html=cover.out