Go语言实用:GoMock基础知识

2,887 阅读5分钟

本文介绍了一下gomock的基础知识:

  • 使用示例
  • Github文档
  • 源码注释

使用示例

gomock是go的一个模拟框架,它很好的集成了Go语言内置的testing包,同时也能在其他的上下文中使用。

安装

Go version < 1.16

GO111MODULE=on go get github.com/golang/mock/mockgen@v1.6.0

Go version 1.16+

go install github.com/golang/mock/mockgen@v1.6.0

项目结构

我们准备如下几个文件:

  • dal/person.go,对应一般项目中与数据库交互或者调用下游RPC服务的层。
  • service/group.go,对应一般项目中的service层,主要用来处理各种业务逻辑。
  • service/group_test.go,单元测试文件

填充内容

dal/person.go:假设我们有一张存储Person信息的表,通过Get(int)方法获取对应的Person。

package dal

type Person interface {
   Get(id int) string
}

service/group.go:我们的业务逻辑处理层。

package service

import "gomockStudy/dal"

type Group struct {
   person dal.Person
}

func (g *Group) GetPerson(id int) string {
   return g.person.Get(id)
}

生成mock文件

回到项目根目录,执行一下命令:

 $mockgen -source=./dal/person.go -destination=./mock_gen/person_mock.go

执行完毕后,我们可以发现多出来了一个文件:mock_gen/person_mock.go

生成mock文件的各参数如下:

  • -source:指定需要模拟(mock)的接口文件
  • -destination:设置生成的mock文件名。若不设置则打印到标准输出中。
  • -packge:设置mock文件的报名。若不设置,则为mock_前缀加文件名。

我们不用关心生成的mock文件细节,只需要知道怎么使用即可。

测试用例

service/group.go

package service

import (
   "github.com/golang/mock/gomock"
   mock_dal "gomockStudy/mock_gen"
   "testing"
)

func TestGroup_GetPerson(t *testing.T) {
   ctl := gomock.NewController(t)

   mockPerson := mock_dal.NewMockPerson(ctl)
   mockPerson.EXPECT().Get(gomock.Any()).Return("person1").AnyTimes()

   group := Group{mockPerson}

   person := group.GetPerson(1)
   if person != "person1" {
      t.Errorf("group.GetPerson id = 1, result = %v", person)
   }

   person = group.GetPerson(2)
   if person != "person2" {
      t.Errorf("group.GetPerson id = 2, result = %v", person)
   }
}
  1. gomock.NewController:Controller表示模拟生态系统的顶级控制。它定义模拟对象的作用域、生命周期,以及它们的期望。从多个goroutine调用Controller的方法是线程安全的。每个测试都应该创建一个新的Controller。
  2. mock_dal.NewMockPerson:创建一个模拟(mock)的Person对象。
  3. 第13行:
    1. EXPECT():返回一个对象,该对象允许调用者设置期望的返回值
    2. Get():设置入参并调用mock对象中的方法。gomock.Any()表示匹配任意入参。
    3. Return():设置返回值。
    4. AnyTimes():无限次调用,也就是不管调用多少次都会返回这个结果。

注意:在go 1.14+时,如果已经将*testing.T对象传入Controller,可以不用主动调用Finish()

测试

回到项目根目录,执行一下命令:

$go test ./service                                                     
--- FAIL: TestGroup_GetPerson (0.00s)
    group_test.go:24: group.GetPerson id = 2, result = person1
FAIL
FAIL    gomockStudy/service     0.560s
FAIL

可以看到,有一行期望返回person2,但是返回了person1。修改测试文件中的第13行:

mockPerson.EXPECT().Get(1).Return("person1")
mockPerson.EXPECT().Get(2).Return("person2")

可以得到测试成功的结果:

$go test ./service
ok      gomockStudy/service     0.689s

至此,我们已经学会了如何使用模拟(mock)对象来帮助我们进行单元测试了。

下面,我们来看一下,gomock的github主页上是怎么介绍的。

Git主页

运行mockgen

mockgen有两种执行模式:sourcereflect

Source mode

源码模式通过源代码生成对应的模拟接口,通过使用-source参数来使用。在这种模式下,有可能会用到-imports-aux_files参数。

mockgen -source=foo.go [other options]

Reflect mode

反射模式通过构建一个使用反射来解析接口的程序来生成模拟接口。它通过传递两个非标志参数来启用:导入路径和逗号分隔的符号列表。

可以使用"."表示当前路径的包。

mockgen database/sql/driver Conn,Driver

# Convenient for `go:generate`.
mockgen . Conn,Driver

Flags

mockgen命令用于在Go源文件中包含要模拟的接口的情况下为模拟类生成源代码。它支持以下flags:

  • -source:包含要mock的接口的文件。
  • -destination:将生成的源代码写入其中的文件。如果不设置此参数,则将代码打印为标准输出。
  • -package:用于生成模拟类源代码的包。如果不设置此参数,则包名为mock_与输入文件的包连接。
  • -imports:在生成的源代码中应该使用的一个显式导入列表,指定为一个以逗号分隔的元素列表,形式为foo=bar/baz,其中bar/baz是被导入的包,foo是生成的源代码中包使用的标识符。
  • -aux_files:需要查询的附加文件列表,例如在不同文件中定义的嵌入式接口。它被指定为一个以逗号分隔的元素列表,形式为foo=bar/baz,bar/baz.go是源文件,foo是-source文件使用的那个文件的包名。
  • -build_flags:(仅反射模式)标记逐字传递到构建。
  • -mock_names:生成的模拟对象的自定义名称列表。形如以逗号分割的键值对列表:

Repository=MockSensorRepository,Endpoint=MockSensorEndpoint。其中,Repository是接口名,MockSensorRepository是与生成的模拟名。如果其中一个接口没有指定自定义名称,那么将使用默认命名约定。

  • -self_package:生成代码的完整包导入路径。这个标志的目的是通过尝试包含它自己的包来防止生成代码中的导入死循环。如果mock的包被设置为它的一个输入(通常是主输入),并且输出是stdio,因此mockgen无法检测到最终的输出包,就会发生这种情况。设置这个标志将告诉mockgen要排除哪个导入。
  • -copyright_file:用于向生成的源代码中添加版权头的版权文件。
  • -debug_parser:只打印解析器结果。
  • -exec_only:(反射模式)如果设置,执行反射程序。
  • -prog_only:(反射模式)只生成反射程序;将其写入stdout并退出。
  • -write_package_comment:如果为true,编写包文档注释(godoc)。(默认为true)

构建Mocks

第一节的例子比这一节要详细,这边就不列了。

修改失败消息

当匹配器报告失败时,它将打印接收到的值(Got)和期望的值(Want)。

Got: [3]
Want: is equal to 2
Expected call at user_test.go:33 doesn't match the argument at index 1.
Got: [0 1 1 2 3]
Want: is equal to 1

修改Want

Want值来自匹配器的String()方法。如果匹配器的默认输出不满足你的需要,那么它可以被修改如下:

gomock.WantFormatter(
    gomock.StringerFunc(func() string { return "is equal to fifteen" }),
    gomock.Eq(15),
)

修改后的gomock.Eq(15)的打印信息将由is equal to 15变为is equal to fifteen

修改Got

Got值来自对象的String()方法,如果它是可用的。在某些情况下,对象的输出很难读取(例如,[]byte),因此测试以不同的方式打印它将会很有帮助。下面的命令修改了Got的格式:

gomock.GotFormatterAdapter(
    gomock.GotFormatterFunc(func(i interface{}) string {
        return fmt.Sprintf("%02d", i)
    }),
    gomock.Eq(15),
)

如果接收到的值是3,那么它将被打印为03。

调试Errors

反射vendoring错误

cannot find package "."
... github.com/golang/mock/mockgen/model

如果你在使用反射模式和vendoring依赖时遇到这个错误,有三个解决方案:

  1. 使用source mode
  2. 导入空的导入:import _ "github/golang/mock/mockgen/model"
  3. 添加--build_flags=--mod=mod参数。

这个错误是由于在最近的版本中go命令的默认行为发生了变化。

源码注释

在gomock的源码的注释中,也介绍了一部分相关的知识。

执行顺序

默认情况下,预期的调用不会强制以任何特定的顺序运行。

Call顺序依赖关系可以通过使用InOrder和或Call.After()来强制约束。

Call.After()调用可以创建更多样化的调用顺序依赖关系,但InOrder通常更方便。

After

firstCall := mockObj.EXPECT().SomeMethod(1, "first")
secondCall := mockObj.EXPECT().SomeMethod(2, "second").After(firstCall)
mockObj.EXPECT().SomeMethod(3, "third").After(secondCall)

InOrder

gomock.InOrder(
    mockObj.EXPECT().SomeMethod(1, "first"),
    mockObj.EXPECT().SomeMethod(2, "second"),
    mockObj.EXPECT().SomeMethod(3, "third"),
)