go mock使用介绍
单元测试需要mock一些东西(比如,RPC的远程调用),这里介绍goMock的使用方式。
安装
需要安装两个东西
-
gomock的包
用来做mock代码库的。
仓库:github.com/golang/mock/gomock
go get github.com/golang/mock/gomock
-
gogen工具
仓库:github.com/golang/mock/mockgen
一个工具,用来帮助生成mock代码的。理论上来说,不用这个工具是可以的,自己用手写。
go版本 < 1.16
GO111MODULE=on go get github.com/golang/mock/mockgen@v1.6.0
go版本 > 1.16
go install github.com/golang/mock/mockgen@v1.6.0
验证是否安装成功,在命令行输入 mockgen.exe
(windows系统),或者mockgen
(mac),会有一些输出。
整个项目如下:
基础使用
使用gomock的时候有几个步骤
- 使用gogen生成需要mock的接口。
- 创建
gomock.Controller
,将它传递给我需要mock的对象。用来做mock对象的总控。 - 调用
EXPECT()
,来模拟期望输入和输出。 - 在
gomock.Controller
调用Finish
。
来个例子看看。
user想要获取通过id来获取还没有付钱的订单信息,OrderService是远程接口,现在需要测试,user的listUserNoPayOrderDetail
方法,自己需要mockOrderService
接口。
先在项目的根目录下面从创建一个mocks包,之后通过mockgen生成对OrderService
接口的mock方法,在user的测试中使用。按照下面的步骤来
-
在项目的根目录下面,执行下面命令
mockgen.exe -destination="mocks/mock_orderService.go" -package="mocks" goMockTest/order OrderService
命令说明
- destination:mockgen生成的文件存放的位置以及文件的名字。
- package:生成的mock文件的包名。
- goMockTest/order:module名字+ 包名字。
- OrderService:需要mock的接口的名称。(goMockTest/order OrderService的意思是需要mock的接口是哪个模块下哪个包里面的哪个接口)
- 其他的
- imports:在生成的mock文件中导入的包。
- write_package_comment:是否要写注释,默认是true。
- 还有一些看官网
上面的命令的意思是:利用mockgen,生成对goMockTest模块order包里面的OrderService接口的mock文件,mock文件存放在当前项目下面mock文件夹下,文件名为mock_orderService.go。生成的mock文件的包名为mocks
在后续的使用中,就可以利用它了。
比如,现在要测试User的userNoPayTotalPrice方法。
单元测试如下:
func TestUser_userNoPayTotalPrice(t *testing.T) {
//创建MockController
controller := gomock.NewController(t)
// 调用finish方法
defer controller.Finish()
// 利用MockController来创建mock的OrderService接口
mockOrderService := mocks.NewMockOrderService(controller)
// mock的接口的返回值
res := []order.OrderInfo{
{
Id: 1,
Goods: []uint64{1,2,3,4,5},
Price: 100,
},
{
Id: 2,
Goods: []uint64{1,2,3,5},
Price: 80,
},
}
//mock ListOrderInfoByUserIdAndOrderStatus 方法,期望输入参数为1,和noPay,返回为res,调用一次
mockOrderService.EXPECT().ListOrderInfoByUserIdAndOrderStatus(1,order.NO_PAY).Return(res).Times(1)
// 将mock出来的orderService创建User
user := User{
mockOrderService,
}
// 调用user的方法
assert.Equal(t, float64(180),user.userNoPayTotalPrice(1),"啦啦啦啦")
}
对上面代码的说明:
-
需要先创建mock controller,Controller定义mock对象的作用域,期望的输入输出,从多个go goroutines中调用Controller是安全的,每个单元测试需要创建一个goMock,并且通过defer调用它的Finish方法。
-
利用controller创建mockOrderService。
-
调用mock对象的EXPECT方法指定期望输入和输出
对于上面的例子:
我想mock
OrderServce
的ListOrderInfoByUserIdAndOrderStatus
方法。输入为1,和noPay,返回为一个[]order.OrderInfo
,调用一次。mockOrderService.EXPECT().ListOrderInfoByUserIdAndOrderStatus(1,order.NO_PAY).Return(res).Times(1)
可以通过
Times(number),MaxTimes(number),MinTimes(number)
方法来限制调用的次数。如果不指定Times,默认的是1次。
单元测试结果如下:
可以直接在goland中直接允许,也可以在命令行中运行 go test -v ./...
(go test默认不会往下找所有的单元测试。要不就进去单元测试所在的包,要不就运行,要不就在运行的时候指定包)
注意,这里使用的assert,需要下载 go get github.com/stretchr/testify/assert
高阶使用
参数匹配
上面的ListOrderInfoByUserIdAndOrderStatus
限定了入参,比如为1和order.NO_PAY,要是这里允许1,2,3呢?需要怎么做呢?或者想要通过参数类型来匹配,或者要对参数做自定义的判断(比如,参数a比如大于参数b,或者参数要进行枚举校验)这些要怎么做呢?gomock提供的参数匹配可以做这件事情。
gomock.Any()
: 匹配任何值。gomock.Eq(x)
: 使用反射来组深度匹配。gomock.Nil()
: nilgomock.Not(m)
: 不是m
例子,比如,ListOrderInfoByUserIdAndOrderStatus
方法,第一个参数可以是任意值。
mockOrderService.EXPECT().ListOrderInfoByUserIdAndOrderStatus(gomock.Any(),order.NO_PAY).Return(res)
第一个参数类型长度为1
mockOrderService.EXPECT().ListOrderInfoByUserIdAndOrderStatus(gomock.Len(1),order.NO_PAY).Return(res)
参数可以是任意值
mockOrderService.EXPECT().ListOrderInfoByUserIdAndOrderStatus(gomock.Any(),gomock.Any()).Return(res)
要是这些不满足,还可以自定义Match来做匹配
自定义Match做匹配
只要实现Match接口,自定义就好了。比如,现在要求做类型匹配
提供OfType
函数,返回match实现类,重写match方法和String方法,一个用来做匹配,String方法用来做提示。
使用方式如下:
第一个参数必须是String类型,第二个参数可以是任意值
mockOrderService.EXPECT().ListOrderInfoByUserIdAndOrderStatus(match.OfType("1"),gomock.Any()).Return(res)
匹配失败:
更灵活的操作
如果要比较两个参数之间的关系,参数a要比参数b小。就需要通过gomock提供的DO方法来做。
func TestUser_userNoPayTotalPrice(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
mockOrderService := mocks.NewMockOrderService(controller)
res := []order.OrderInfo{
{
Id: 1,
Goods: []uint64{1,2,3,4,5},
Price: 100,
},
{
Id: 2,
Goods: []uint64{1,2,3,5},
Price: 80,
},
}
//mockOrderService.EXPECT().ListOrderInfoByUserIdAndOrderStatus(match.OfType("1"),gomock.Any()).Return(res)
mockOrderService.EXPECT().
ListOrderInfoByUserIdAndOrderStatus(match.OfType(1),gomock.Any()).
Return(res).
Do(func(a int,b any) {
if a <= b.(int){
// 做参数校验
t.Fatalf("%s","非法的参数")
}
})
user := User{
mockOrderService,
}
assert.Equal(t, float64(180),user.userNoPayTotalPrice(-1),"啦啦啦啦")
}
很灵活的做参数的处理了。
根据不同的入参返回不同的值
gomock提供了DoAndReturn
方法来做。
func TestUser_userNoPayTotalPrice(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
mockOrderService := mocks.NewMockOrderService(controller)
res := []order.OrderInfo{
{
Id: 1,
Goods: []uint64{1,2,3,4,5},
Price: 100,
},
{
Id: 2,
Goods: []uint64{1,2,3,5},
Price: 80,
},
}
//mockOrderService.EXPECT().ListOrderInfoByUserIdAndOrderStatus(match.OfType("1"),gomock.Any()).Return(res)
mockOrderService.EXPECT().
ListOrderInfoByUserIdAndOrderStatus(match.OfType(1),gomock.Any()).
DoAndReturn(func(a int,b any) []order.OrderInfo {
// 1,返回res没否则返回空
if a == 1{
return res
}else{
return []order.OrderInfo{}
}
// 注意这里的AnyTimes,默认调用测试是1次,在一个单元测试里面就只能调用1次,这里anyTimes表示可以调用无限次
}).AnyTimes()
user := User{
mockOrderService,
}
assert.Equal(t, float64(180),user.userNoPayTotalPrice(1),"啦啦啦啦")
assert.Equal(t, float64(0),user.userNoPayTotalPrice(0),"啦啦啦啦")
}
指定调用顺序
还可以指定调用顺序,先调用a,在调用b和c。gomock提供了After
方法。和InOrder
来做这件事情
func TestUser_userNoPayTotalPrice(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
mockOrderService := mocks.NewMockOrderService(controller)
res := []order.OrderInfo{
{
Id: 1,
Goods: []uint64{1,2,3,4,5},
Price: 100,
},
{
Id: 2,
Goods: []uint64{1,2,3,5},
Price: 80,
},
}
// 指定第一个,
first := mockOrderService.EXPECT().
ListOrderInfoByUserIdAndOrderStatus(match.OfType(1), gomock.Any()).
DoAndReturn(func(a int, b any) []order.OrderInfo {
if a == 1 {
return res
} else {
return []order.OrderInfo{}
}
}).AnyTimes()
// 调用 ListAllOrderInfoByUserId得在第一个之后,至于它俩之间的关系,无所谓
mockOrderService.EXPECT().
ListAllOrderInfoByUserId(1).After(first)
// 调用GetOrderInfoByOrderId得在第一个之后
mockOrderService.EXPECT().GetOrderInfoByOrderId(uint64(1)).After(first)
user := User{
mockOrderService,
}
// 如果将ListAllOrderInfoByUserId或者GetOrderInfoByOrderId其中一个放在ListOrderInfoByUserIdAndOrderStatus之前,就会报错。
assert.Equal(t, float64(180),user.userNoPayTotalPrice(1),"啦啦啦啦")
assert.Equal(t, float64(0),user.userNoPayTotalPrice(-1),"啦啦啦啦")
mockOrderService.ListAllOrderInfoByUserId(1)
mockOrderService.GetOrderInfoByOrderId(1)
}
也可以用InOrder
来指定顺序调用。
func TestUser_userNoPayTotalPrice(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
mockOrderService := mocks.NewMockOrderService(controller)
res := []order.OrderInfo{
{
Id: 1,
Goods: []uint64{1,2,3,4,5},
Price: 100,
},
{
Id: 2,
Goods: []uint64{1,2,3,5},
Price: 80,
},
}
//mockOrderService.EXPECT().ListOrderInfoByUserIdAndOrderStatus(match.OfType("1"),gomock.Any()).Return(res)
first := mockOrderService.EXPECT().
ListOrderInfoByUserIdAndOrderStatus(match.OfType(1), gomock.Any()).
DoAndReturn(func(a int, b any) []order.OrderInfo {
if a == 1 {
return res
} else {
return []order.OrderInfo{}
}
}).AnyTimes()
second := mockOrderService.EXPECT().ListAllOrderInfoByUserId(1)
third := mockOrderService.EXPECT().GetOrderInfoByOrderId(uint64(1))
// 按照顺序调用
gomock.InOrder(
first,
second,
third,
)
user := User{
mockOrderService,
}
assert.Equal(t, float64(180),user.userNoPayTotalPrice(1),"啦啦啦啦")
assert.Equal(t, float64(0),user.userNoPayTotalPrice(-1),"啦啦啦啦")
// 不按照顺序执行会报错。
mockOrderService.GetOrderInfoByOrderId(1)
mockOrderService.ListAllOrderInfoByUserId(1)
}
到此,gomock的使用方式就介绍结束。
关于博客这件事,我是把它当做我的笔记,里面有很多的内容反映了我思考的过程,因为思维有限,不免有些内容有出入,如果有问题,欢迎指出。一同探讨。谢谢。