go mock使用介绍

2,127 阅读5分钟

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),会有一些输出。

整个项目如下:

image-20220529101841682.png

基础使用

使用gomock的时候有几个步骤

  • 使用gogen生成需要mock的接口。
  • 创建gomock.Controller,将它传递给我需要mock的对象。用来做mock对象的总控。
  • 调用EXPECT(),来模拟期望输入和输出。
  • gomock.Controller调用Finish

来个例子看看。

image-20220529135756752.png

user想要获取通过id来获取还没有付钱的订单信息,OrderService是远程接口,现在需要测试,user的listUserNoPayOrderDetail方法,自己需要mockOrderService接口。

先在项目的根目录下面从创建一个mocks包,之后通过mockgen生成对OrderService接口的mock方法,在user的测试中使用。按照下面的步骤来

  1. 在项目的根目录下面,执行下面命令

     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

image-20220529140603479.png

在后续的使用中,就可以利用它了。

比如,现在要测试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),"啦啦啦啦")
}

对上面代码的说明:

  1. 需要先创建mock controller,Controller定义mock对象的作用域,期望的输入输出,从多个go goroutines中调用Controller是安全的,每个单元测试需要创建一个goMock,并且通过defer调用它的Finish方法。

  2. 利用controller创建mockOrderService。

  3. 调用mock对象的EXPECT方法指定期望输入和输出

    对于上面的例子:

    我想mock OrderServceListOrderInfoByUserIdAndOrderStatus方法。输入为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默认不会往下找所有的单元测试。要不就进去单元测试所在的包,要不就运行,要不就在运行的时候指定包)

image-20220529142758723.png

​ 注意,这里使用的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(): nil
  • gomock.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接口,自定义就好了。比如,现在要求做类型匹配

image-20220529152826929.png

提供OfType函数,返回match实现类,重写match方法和String方法,一个用来做匹配,String方法用来做提示。

使用方式如下:

第一个参数必须是String类型,第二个参数可以是任意值

mockOrderService.EXPECT().ListOrderInfoByUserIdAndOrderStatus(match.OfType("1"),gomock.Any()).Return(res)

匹配失败:

image-20220529153108784.png

更灵活的操作

如果要比较两个参数之间的关系,参数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的使用方式就介绍结束。


关于博客这件事,我是把它当做我的笔记,里面有很多的内容反映了我思考的过程,因为思维有限,不免有些内容有出入,如果有问题,欢迎指出。一同探讨。谢谢。