Go语言工具包之gomock

Go语言工具包之gomock

1、gomock介绍

GoMock 是由 Golang 官方开发维护的测试框架,实现了较为完整的基于 interface 的 Mock 功能,能够与

Golang 内置的 testing 包良好集成,也能用于其它的测试环境中。GoMock 测试框架包含了 GoMock 包和

mockgen 工具两部分,其中 GoMock 包完成对桩对象生命周期的管理,mockgen 工具用来生成 interface 对应

的 Mock 类源文件,用来辅助生成测试代码。

一般情况下,我们可以使用 Go Test 单元测试进行测试,但是当待测试的函数/对象的依赖关系很复杂,并且有些

依赖不能直接创建,例如数据库连接、文件I/O等。这种场景就非常适合使用 mock/stub 测试。简单来说,就是用

mock 对象模拟依赖项的行为。

gomock地址:https://github.com/golang/mock

1.1 原理

针对 interface 生成对应的 Mock 代码文件,其中包含了一个实现该接口的结构,并提供了操作该结构行为的方

法。使用该结构代替真实的依赖,可以控制下游按我们想要的方式进行某些操作和返回结果,以此达到解除外部依

赖的目的。

1.2 安装

$ go version
go version go1.18.4 windows/amd64
# GoMock安装
$ go get -u github.com/golang/mock/gomock
# mockgen辅助代码生成工具安装
$ go get -u github.com/golang/mock/mockgen
$ go install github.com/golang/mock/mockgen
$ mockgen -version
v1.6.0

GoMock文档:

$ go doc github.com/golang/mock/gomock

2、mockgen参数介绍

mockgen 工具支持的选项如下:

-source:指定接口的源文件。

-destinatio:mock类代码的输出文件,如果没有设置本选项,代码将被输出到标准输出。-destination选项输

入太长,因此推荐使用重定向符号>将输出到标准输出的内容重定向到某个文件,并且mock类代码的输出文件的

路径必须是绝对路径。

-packag:指定 mock 类源文件的包名,如果没有设置本选项,则包名由 mock_ 和输入文件的包名级联而成。

-aux_fi:附加文件列表用于解析嵌套定义在不同文件中的 interface,指定元素列表以逗号分隔,元素形式为

foo=bar/baz.go,其中bar/baz.go是源文件,foo是-source选项指定的源文件用到的包名。

-build_flags:传递给 build 工具的参数。

-imports:依赖的需要 import 的包,在生成的源代码中应该使用的一个显式导入列表,指定为一个以逗号分隔

的元素列表,形式为foo=bar/baz,其中bar/baz是被导入的包,foo是生成的源代码中包使用的标识符。

-mock_names:自定义生成 mock 文件的列表,使用逗号分割。

如 Repository=MockSensorRepository,Endpoint=MockSensorEndpoint。Repository、Endpoint为接口,

MockSensorRepository,MockSensorEndpoint 为相应的 mock 文件。

-self_package:生成代码的完整包导入路径,这个标志的目的是通过尝试包含它自己的包来防止生成代码中的

导入死循环。如果 mock 的包被设置为它的一个输入(通常是主输入),并且输出是 stdio,因此 mockgen 无法检测

到最终的输出包,就会发生这种情况,设置这个标志将告诉 mockgen 要排除哪个导入。

-copyright_file:用于向生成的源代码中添加版权头的版权文件。

-debug_parser:只打印解析器结果。

-exec_only:(反射模式)如果设置,执行反射程序。

-prog_only:(反射模式)只生成反射程序,将其写入stdout并退出。

-write_package_comment:如果为true,编写包文档注释(godoc),默认为true。

3、mockgen工作模式

mockgen 有两种操作模式:源文件模式和反射模式。

3.1 源文件模式

源文件模式通过一个包含 interface 定义的源文件生成 mock 类文件,通过 -source 标识开启,-imports 和

-aux_files 标识在源文件模式下是有用的。mockgen源文件模式的命令格式如下:

mockgen -source=xxxx.go [other options]

3.2 反射模式

反射模式通过构建一个程序用反射理解接口生成一个 mock 类文件,通过两个非标志参数开启:导入路径和用逗

号分隔的符号列表(多个interface)。反射模式的命令格式如下:

mockgen packagepath Interface1,Interface2...

第一个参数是基于 GOPATH 的相对路径,第二个参数可以为多个 interface 并且 interface 之间只能用逗号分隔。

mockgen database/sql/driver Conn,Driver
# 可以使用 . 表示当前路径的包
mockgen . Conn,Driver

4、mockgen工作模式适用场景

mockgen 工作模式适用场景如下:

A、对于简单场景,只需使用 -source 选项。

B、对于复杂场景,如一个源文件定义了多个 interface 而只想对部分 interface 进行 mock,或者 interface 存在

嵌套,则需要使用反射模式。

5、gomock常用方法

Call 表示对 mock 对象的一个期望调用,Call 结构体为:

type Call struct {
   t TestReporter // for triggering test failures on invalid call setup

   receiver   interface{}  // the receiver of the method call
   method     string       // the name of the method
   methodType reflect.Type // the type of the method
   args       []Matcher    // the args
   origin     string       // file and line number of call setup

   preReqs []*Call // prerequisite calls

   // Expectations
   minCalls, maxCalls int

   numCalls int // actual number made

   // actions are called when this Call is called. Each action gets the args and
   // can set the return values by returning a non-nil slice. Actions run in the
   // order they are created.
   actions []func([]interface{}) []interface{}
}

常用方法:

// InOrder声明给定调用的调用顺序
func InOrder(calls ...*Call)

// After声明调用在preReq完成后执行
func (c *Call) After(preReq *Call) *Call

// 允许调用0次或多次
func (c *Call) AnyTimes() *Call

// 声明在匹配时要运行的操作
func (c *Call) Do(f interface{}) *Call

// 设置最大的调用次数为n次
func (c *Call) MaxTimes(n int) *Call

// 设置最小的调用次数为n次
func (c *Call) MinTimes(n int) *Call

// Return声明模拟函数调用返回的值
func (c *Call) Return(rets ...interface{}) *Call

// SetArg声明使用指针设置第n个参数的值
func (c *Call) SetArg(n int, value interface{}) *Call

// 设置调用的次数为n次
func (c *Call) Times(n int) *Call

// 获取控制对象
func NewController(t TestReporter) *Controller

// WithContext返回一个控制器和上下文,如果发生任何致命错误时会取消
func WithContext(ctx context.Context, t TestReporter) (*Controller, context.Context)

// Mock对象调用,不应由用户代码调用
func (ctrl *Controller) Call(receiver interface{}, method string, args ...interface{}) []interface{}

// 检查所有预计调用的方法是否被调用,每个控制器都应该调用,本函数只应该被调用一次
func (ctrl *Controller) Finish()

// 被mock对象调用,不应由用户代码调用
func (ctrl *Controller) RecordCall(receiver interface{}, method string, args ...interface{}) *Call

// 被mock对象调用,不应由用户代码调用
func (ctrl *Controller) RecordCallWithMethodType(receiver interface{}, method string, methodType reflect.Type, args ...interface{}) *Call

// 匹配任意值
func Any() Matcher

// AssignableToTypeOf是一个匹配器,用于匹配赋值给模拟调用函数的参数和函数的参数类型是否匹配
func AssignableToTypeOf(x interface{}) Matcher

// 通过反射匹配到指定的类型值,而不需要手动设置
func Eq(x interface{}) Matcher

// 返回nil
func Nil() Matcher

// 不递归给定子匹配器的结果
func Not(x interface{}) Matcher

6、gomock应用示例

使用 gomock 的时候有几个步骤:

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

让我们通过一个简单的例子来演示 gomock 的整个使用流程,为简单起见我们只看两个文件,一个是接口文件,

user/inter.go,是我们希望 mock 的接口;另一个是结构体文件,user/user.go,是使用了 iuser 接口的结

构体 User。

6.1 创建 interface

package user

type Inter interface {
	DoResData(int, string) error
}

6.2 创建结构体

package user

type User struct {
	Inter Inter
}

func (user *User) Use() error {
	return user.Inter.DoResData(123, "Hello GoMock")
}

6.3 mock文件生成

先在项目的根目录下创建一个 mocks 包,然后通过 mockgen 生成对 Inter 接口的 mock 方法,在 user 的测试中

使用。按照下面的步骤来:

$ mockgen -destination="mocks/mock_inter.go" -package="mocks" -source ./user/inter.go Inter

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

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

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

生成的 mocks/mock_inter.go 文件的内容:

// Code generated by MockGen. DO NOT EDIT.
// Source: ./user/inter.go

// Package mocks is a generated GoMock package.
package mocks

import (
	reflect "reflect"

	gomock "github.com/golang/mock/gomock"
)

// MockInter is a mock of Inter interface.
type MockInter struct {
	ctrl     *gomock.Controller
	recorder *MockInterMockRecorder
}

// MockInterMockRecorder is the mock recorder for MockInter.
type MockInterMockRecorder struct {
	mock *MockInter
}

// NewMockInter creates a new mock instance.
func NewMockInter(ctrl *gomock.Controller) *MockInter {
	mock := &MockInter{ctrl: ctrl}
	mock.recorder = &MockInterMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockInter) EXPECT() *MockInterMockRecorder {
	return m.recorder
}

// DoResData mocks base method.
func (m *MockInter) DoResData(arg0 int, arg1 string) error {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "DoResData", arg0, arg1)
	ret0, _ := ret[0].(error)
	return ret0
}

// DoResData indicates an expected call of DoResData.
func (mr *MockInterMockRecorder) DoResData(arg0, arg1 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoResData", reflect.TypeOf((*MockInter)(nil).DoResData), arg0, arg1)
}

6.4 创建测试用例

package user_test

import (
	"github.com/golang/mock/gomock"
	"proj/mocks"
	"proj/user"
	"testing"
)

func TestUse(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	// 先设置期望的返回结果
	mockInter := mocks.NewMockInter(mockCtrl)
	mockInter.EXPECT().DoResData(123, "Hello GoMock").Return(nil).Times(1)

	// 再调用方法
	testUser := &user.User{Inter: mockInter}
	err := testUser.Use()
	if err != nil {
		t.Errorf("result = %v", err)
	}
}

进行测试:

$ go test -v
=== RUN   TestUse
--- PASS: TestUse (0.00s)
PASS
ok      proj/user       0.026s

对上面代码的说明:

1 、gomock.NewController(t):Controller 表示模拟生态系统的顶级控制,它定义模拟对象的作用域、生命周

期,以及它们的期望。从多个 goroutine 调用 Controller 的方法是线程安全的,每个测试都应该创建一个新的

Controller,并且通过 defer 调用它的 Finish 方法。

2、mocks.NewMockInter(mockCtrl):创建一个模拟(mock)的 Inter 对象。

3、EXPECT():返回一个 erro 对象,该对象允许调用者设置期望的返回值。

4、DoResData():设置入参并调用 mock 对象中的方法。

5、gomock.Any():可以用来匹配任意入参,上面使用的是固定参数。

6、Return():设置返回值。

7、Times(1):调用一次,也可以设置成 AnyTimes() 进行无限次调用,也就是不管调用多少次都会返回这个结

果。

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

7、打桩

再看一个简单的例子:

package db

type DB interface {
	Get(key string) (int, error)
}

func GetFromDB(db DB, key string) int {
	if value, err := db.Get(key); err == nil {
		return value
	}

	return -1
}

生成 mock :

$ mockgen -destination="mocks/mock_db.go" -package="mocks" -source ./db/db.go DB

生成的文件 mocks/db.go 的内容为:

// Code generated by MockGen. DO NOT EDIT.
// Source: ./db/db.go

// Package mocks is a generated GoMock package.
package mocks

import (
	reflect "reflect"

	gomock "github.com/golang/mock/gomock"
)

// MockDB is a mock of DB interface.
type MockDB struct {
	ctrl     *gomock.Controller
	recorder *MockDBMockRecorder
}

// MockDBMockRecorder is the mock recorder for MockDB.
type MockDBMockRecorder struct {
	mock *MockDB
}

// NewMockDB creates a new mock instance.
func NewMockDB(ctrl *gomock.Controller) *MockDB {
	mock := &MockDB{ctrl: ctrl}
	mock.recorder = &MockDBMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockDB) EXPECT() *MockDBMockRecorder {
	return m.recorder
}

// Get mocks base method.
func (m *MockDB) Get(key string) (int, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Get", key)
	ret0, _ := ret[0].(int)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Get indicates an expected call of Get.
func (mr *MockDBMockRecorder) Get(key interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockDB)(nil).Get), key)
}

编写测试类:

package db_test

import (
	"errors"
	"github.com/golang/mock/gomock"
	"proj/db"
	"proj/mocks"
	"testing"
)

func TestGetFromDB(t *testing.T) {
	ctrl := gomock.NewController(t)
	// 断言DB.Get()方法是否被调用
	defer ctrl.Finish()

	// 如果DB.Get()返回error,那么GetFromDB()返回-1
	m := mocks.NewMockDB(ctrl)
	m.EXPECT().Get(gomock.Eq("Tom")).Return(100, errors.New("not exist"))

	if v := db.GetFromDB(m, "Tom"); v != -1 {
		t.Fatal("expected -1, but got", v)
	}
}

在上面的例子中,当 Get() 的参数为 Tom,则返回 error,这称之为 打桩(stub),有明确的参数和返回值是最

简单打桩方式。除此之外,检测调用次数、调用顺序,动态设置返回值等方式也经常使用。

7.1 参数(Eq,Any,Not,Nil)

m.EXPECT().Get(gomock.Eq("Tom")).Return(0, errors.New("not exist"))
m.EXPECT().Get(gomock.Any()).Return(630, nil)
m.EXPECT().Get(gomock.Not("Sam")).Return(0, nil)
m.EXPECT().Get(gomock.Nil()).Return(0, errors.New("nil"))
  • Eq(value):表示与 value 等价的值。
  • Any():可以用来表示任意的入参。
  • Not(value):用来表示非 value 以外的值。
  • Nil():表示 None 值

7.2 返回值(Return,DoAndReturn)

m.EXPECT().Get(gomock.Not("Sam")).Return(0, nil)
m.EXPECT().Get(gomock.Any()).Do(func(key string) {
   t.Log(key)
})
m.EXPECT().Get(gomock.Any()).DoAndReturn(func(key string) (int, error) {
   if key == "Sam" {
      return 630, nil
   }
   return 0, errors.New("not exist")
})
  • Retur:返回确定的值。
  • Do:Mock 方法被调用时,要执行的操作吗,忽略返回值。
  • DoAndRetur:可以动态地控制返回值。

7.3 调用次数(Times)

func TestGetFromDB(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	m := mocks.NewMockDB(ctrl)
	m.EXPECT().Get(gomock.Not("Sam")).Return(0, nil).Times(2)
	db.GetFromDB(m, "ABC")
	db.GetFromDB(m, "DEF")
}
  • Times():断言 Mock 方法被调用的次数。
  • MaxTimes():最大次数。
  • MinTimes():最小次数。
  • AnyTimes():任意次数(包括 0 次)。

7.4 执行顺序(InOrder)

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

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

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

func TestGetFromDB(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish() // 断言 DB.Get() 方法是否被调用

	m := mocks.NewMockDB(ctrl)
	o1 := m.EXPECT().Get(gomock.Eq("Tom")).Return(0, errors.New("not exist"))
	o2 := m.EXPECT().Get(gomock.Eq("Sam")).Return(630, nil)
	gomock.InOrder(o1, o2)
	db.GetFromDB(m, "Tom")
	db.GetFromDB(m, "Sam")
}
7.4.1 After
firstCall := mockObj.EXPECT().SomeMethod(1, "first")
secondCall := mockObj.EXPECT().SomeMethod(2, "second").After(firstCall)
mockObj.EXPECT().SomeMethod(3, "third").After(secondCall)
7.4.2 InOrder
gomock.InOrder(
    mockObj.EXPECT().SomeMethod(1, "first"),
    mockObj.EXPECT().SomeMethod(2, "second"),
    mockObj.EXPECT().SomeMethod(3, "third"),
)

8、测试结果查看

生成测试覆盖率的 profile 文件:

go test -coverprofile=cover.out .

利用 profile 文件生成可视化界面:

go tool cover -html=cover.out

你可能感兴趣的:(golang,golang)