Go语言开发(二十一)、GoMock测试框架

栏目: Go · 发布时间: 6年前

内容简介:定义一个需要mock的接口Repository,infra/db.go文件如下:生成mock文件后就可以使用mock对象进行打桩测试,编写测试用例。

Go语言开发(二十一)、GoMock测试框架

一、GoMock简介

1、GoMock简介

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

GoMock文档:

go doc github.com/golang/mock/gomock

2、mockgen使用

(1)mockgen工具选项

mockgen工具支持的选项如下:

-source: 指定接口的源文件

-destination: mock类代码的输出文件。如果没有设置本选项,代码将被输出到标准输出。-destination选项输入太长,因此推荐使用重定向符号>将输出到标准输出的内容重定向到某个文件,并且mock类代码的输出文件的路径必须是绝对路径。

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

-aux_files: 附加文件列表用于解析嵌套定义在不同文件中的interface。指定元素列表以逗号分隔,元素形式为foo=bar/baz.go,其中bar/baz.go是源文件,foo是-source选项指定的源文件用到的包名。

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

-imports: 依赖的需要import的包

-mock_names:自定义生成mock文件的列表,使用逗号分割。如Repository=MockSensorRepository,Endpoint=MockSensorEndpoint。

Repository、Endpoint为接口,MockSensorRepository,MockSensorEndpoint为相应的mock文件。

(2)mockgen工作模式

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

源文件模式通过一个包含interface定义的源文件生成mock类文件,通过-source标识开启,-imports和-aux_files标识在源文件模式下是有用的。mockgen源文件模式的命令格式如下:

mockgen -source=xxxx.go [other options]

反射模式通过构建一个程序用反射理解接口生成一个mock类文件,通过两个非标志参数开启:导入路径和用逗号分隔的符号列表(多个interface)。

mockgen反射模式的命令格式如下:

mockgen packagepath Interface1,Interface2...

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

(3)mockgen工作模式适用场景

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

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

B、对于复杂场景,如一个源文件定义了多个interface而只想对部分interface进行mock,或者interface存在嵌套,则需要使用反射模式。

二、GoMock常用方法

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

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{}
}

Call表示对mock对象的一个期望调用

func (c *Call) After(preReq *Call) *Call

After声明调用在preReq完成后执行

func (c *Call) AnyTimes() *Call

允许调用0次或多次

func (c *Call) Do(f interface{}) *Call

声明在匹配时要运行的操作

func (c *Call) MaxTimes(n int) *Call

设置最大的调用次数为n次

func (c *Call) MinTimes(n int) *Call

设置最小的调用次数为n次

func (c *Call) Return(rets ...interface{}) *Call

Return声明模拟函数调用返回的值

func (c *Call) SetArg(n int, value interface{}) *Call

SetArg声明使用指针设置第n个参数的值

func (c *Call) Times(n int) *Call

设置调用的次数为n次

func NewController(t TestReporter) *Controller

获取控制对象

func WithContext(ctx context.Context, t TestReporter) (*Controller, context.Context)

WithContext返回一个控制器和上下文,如果发生任何致命错误时会取消。

func (ctrl *Controller) Call(receiver interface{}, method string, args ...interface{}) []interface{}

Mock对象调用,不应由用户代码调用。

func (ctrl *Controller) Finish()

检查所有预计调用的方法是否被调用,每个控制器都应该调用。本函数只应该被调用一次。

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

被mock对象调用,不应由用户代码调用。

func Any() Matcher

匹配任意值

func AssignableToTypeOf(x interface{}) Matcher

AssignableToTypeOf是一个匹配器,用于匹配赋值给模拟调用函数的参数和函数的参数类型是否匹配。

func Eq(x interface{}) Matcher

通过反射匹配到指定的类型值,而不需要手动设置

func Nil() Matcher

返回nil

func Not(x interface{}) Matcher

不递归给定子匹配器的结果

三、GoMock应用示例

1、interface编写

定义一个需要mock的接口Repository,infra/db.go文件如下:

package db

type Repository interface {
   Create(key string, value []byte) error
   Retrieve(key string) ([]byte, error)
   Update(key string, value []byte) error
   Delete(key string) error
}

2、mock文件生成

mockgen生成mock文件:

mockgen -source=./infra/db.go -destination=./mock/mock_repository.go -package=mock

输出目录./mock必须存在,否则mockgen会运行失败。

如果工程中的第三方库统一放在vendor目录下,则需要拷贝一份gomock代码到 $GOPATH/src/github.com/golang/mock/gomock ,mockgen命令运行时会在上述路径访问gomock。

mock_repository.go文件如下:

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

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

import (
   gomock "github.com/golang/mock/gomock"
   reflect "reflect"
)

// MockRepository is a mock of Repository interface
type MockRepository struct {
   ctrl     *gomock.Controller
   recorder *MockRepositoryMockRecorder
}

// MockRepositoryMockRecorder is the mock recorder for MockRepository
type MockRepositoryMockRecorder struct {
   mock *MockRepository
}

// NewMockRepository creates a new mock instance
func NewMockRepository(ctrl *gomock.Controller) *MockRepository {
   mock := &MockRepository{ctrl: ctrl}
   mock.recorder = &MockRepositoryMockRecorder{mock}
   return mock
}

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

// Create mocks base method
func (m *MockRepository) Create(key string, value []byte) error {
   ret := m.ctrl.Call(m, "Create", key, value)
   ret0, _ := ret[0].(error)
   return ret0
}

// Create indicates an expected call of Create
func (mr *MockRepositoryMockRecorder) Create(key, value interface{}) *gomock.Call {
   return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRepository)(nil).Create), key, value)
}

// Retrieve mocks base method
func (m *MockRepository) Retrieve(key string) ([]byte, error) {
   ret := m.ctrl.Call(m, "Retrieve", key)
   ret0, _ := ret[0].([]byte)
   ret1, _ := ret[1].(error)
   return ret0, ret1
}

// Retrieve indicates an expected call of Retrieve
func (mr *MockRepositoryMockRecorder) Retrieve(key interface{}) *gomock.Call {
   return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Retrieve", reflect.TypeOf((*MockRepository)(nil).Retrieve), key)
}

// Update mocks base method
func (m *MockRepository) Update(key string, value []byte) error {
   ret := m.ctrl.Call(m, "Update", key, value)
   ret0, _ := ret[0].(error)
   return ret0
}

// Update indicates an expected call of Update
func (mr *MockRepositoryMockRecorder) Update(key, value interface{}) *gomock.Call {
   return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRepository)(nil).Update), key, value)
}

// Delete mocks base method
func (m *MockRepository) Delete(key string) error {
   ret := m.ctrl.Call(m, "Delete", key)
   ret0, _ := ret[0].(error)
   return ret0
}

// Delete indicates an expected call of Delete
func (mr *MockRepositoryMockRecorder) Delete(key interface{}) *gomock.Call {
   return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository)(nil).Delete), key)
}

3、MySQL.go文件

package MySQL

import "GoExample/GoMock/infra"

type MySQL struct {
   DB db.Repository
}

func NewMySQL(db db.Repository) *MySQL {
   return &MySQL{DB: db}
}

func (mysql *MySQL) CreateData(key string, value []byte) error {
   return mysql.DB.Create(key, value)
}

func (mysql *MySQL) GetData(key string) ([]byte, error) {
   return mysql.DB.Retrieve(key)
}

func (mysql *MySQL) DeleteData(key string) error {
   return mysql.DB.Delete(key)
}

func (mysql *MySQL) UpdateData(key string, value []byte) error {
   return mysql.DB.Update(key, value)
}

4、测试用例编写

生成mock文件后就可以使用mock对象进行打桩测试,编写测试用例。

(1)导入mock相关包

mock相关包包括testing,gomock和mock,import包路径:

import (
   "testing"
   "GoExample/GoMock/mock"
   "github.com/golang/mock/gomock"
)

(2)mock控制器

mock控制器通过NewController接口生成,是mock生态系统的顶层控制,定义了mock对象的作用域和生命周期,以及mock对象的期望。多个协程同时调用控制器的方法是安全的。当用例结束后,控制器会检查所有剩余期望的调用是否满足条件。

ctrl := NewController(t)
defer ctrl.Finish()

mock对象创建时需要注入控制器,mock对象注入控制器的代码如下:

ctrl := NewController(t)
defer ctrl.Finish()
mockRepo := mock_db.NewMockRepository(ctrl)

(3)mock对象的行为注入

对于mock对象的行为注入,控制器通过map来维护,一个方法对应map的一项。因为一个方法在一个用例中可能调用多次,所以map的值类型是数组切片。当mock对象进行行为注入时,控制器会将行为Add。当该方法被调用时,控制器会将该行为Remove。

如果先Retrieve领域对象失败,然后Create领域对象成功,再次Retrieve领域对象就能成功。mock对象的行为注入代码如下所示:

mockRepo.EXPECT().Retrieve(Any()).Return(nil, ErrAny)
mockRepo.EXPECT().Create(Any(), Any()).Return(nil)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes, nil)

当批量Create对象时,可以使用Times关键字:

mockRepo.EXPECT().Create(Any(), Any()).Return(nil).Times(5)

当批量Retrieve对象时,需要注入多次mock行为:

mockRepo.EXPECT().Retrieve(Any()).Return(objBytes1, nil)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes2, nil)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes3, nil)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes4, nil)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes5, nil)

(4)行为调用的保序

默认情况下,行为调用顺序可以和mock对象行为注入顺序不一致,即不保序。如果要保序,有两种方法:

A、通过After关键字来实现保序

B、通过InOrder关键字来实现保序

通过After关键字实现的保序示例代码:

retrieveCall := mockRepo.EXPECT().Retrieve(Any()).Return(nil, ErrAny)
createCall := mockRepo.EXPECT().Create(Any(), Any()).Return(nil).After(retrieveCall)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes, nil).After(createCall)

通过InOrder关键字实现的保序示例代码:

InOrder(
mockRepo.EXPECT().Retrieve(Any()).Return(nil, ErrAny)
mockRepo.EXPECT().Create(Any(), Any()).Return(nil)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes, nil)
)

通过InOrder关键字实现保序更简单,关键字InOrder是After的语法糖。

func InOrder(calls ...*Call) {
   for i := 1; i < len(calls); i++ {
      calls[i].After(calls[i-1])
   }
}

当mock对象行为的注入保序后,如果行为调用的顺序和其不一致,就会触发测试失败。如果在测试用例执行过程中,Repository方法的调用顺序如果不是按 Retrieve -> Create -> Retrieve的顺序进行,则会导致测试失败。

(5)mock对象的注入

mock对象的行为都注入到控制器后,要将mock对象注入给interface,使得mock对象在测试中生效。

通常,当测试用例执行完成后,并没有回滚interface到真实对象,有可能会影响其它测试用例的执行,因此推荐使用GoStub框架完成mock对象的注入。

stubs := StubFunc(&mysql,mockdb)
defer stubs.Reset()

(6)测试用例编写

MySQL_test.go文件:

package MySQL

import (
   "testing"

   "GoExample/GoMock/mock"

   "fmt"

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

func TestMySQL_CreateData(t *testing.T) {
   ctr := gomock.NewController(t)
   defer ctr.Finish()
   var key string = "Hello"
   var value []byte = []byte("Go")
   mockRepository := mock_db.NewMockRepository(ctr)
   gomock.InOrder(
      mockRepository.EXPECT().Create(key, value).Return(nil),
   )
   mySQL := NewMySQL(mockRepository)
   err := mySQL.CreateData(key, value)
   if err != nil {
      fmt.Println(err)
   }
}

func TestMySQL_GetData(t *testing.T) {
   ctr := gomock.NewController(t)
   defer ctr.Finish()
   var key string = "Hello"
   var value []byte = []byte("Go")
   mockRepository := mock_db.NewMockRepository(ctr)
   gomock.InOrder(
      mockRepository.EXPECT().Retrieve(key).Return(value, nil),
   )
   mySQL := NewMySQL(mockRepository)
   bytes, err := mySQL.GetData(key)
   if err != nil {
      fmt.Println(err)
   } else {
      fmt.Println(string(bytes))
   }
}

func TestMySQL_UpdateData(t *testing.T) {
   ctr := gomock.NewController(t)
   defer ctr.Finish()
   var key string = "Hello"
   var value []byte = []byte("Go")
   mockRepository := mock_db.NewMockRepository(ctr)
   gomock.InOrder(
      mockRepository.EXPECT().Update(key, value).Return(nil),
   )
   mySQL := NewMySQL(mockRepository)
   err := mySQL.UpdateData(key, value)
   if err != nil {
      fmt.Println(err)
   }
}

func TestMySQL_DeleteData(t *testing.T) {
   ctr := gomock.NewController(t)
   defer ctr.Finish()
   var key string = "Hello"
   mockRepository := mock_db.NewMockRepository(ctr)
   gomock.InOrder(
      mockRepository.EXPECT().Delete(key).Return(nil),
   )
   mySQL := NewMySQL(mockRepository)
   err := mySQL.DeleteData(key)
   if err != nil {
      fmt.Println(err)
   }
}

5、测试

进入测试用例目录:

go test .

6、测试结果查看

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

go test -coverprofile=cover.out .

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

go tool cover -html=cover.out
Go语言开发(二十一)、GoMock测试框架

以上所述就是小编给大家介绍的《Go语言开发(二十一)、GoMock测试框架》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

GWT in Action

GWT in Action

Robert Hanson、Adam Tacy / Manning Publications / 2007-06-05 / USD 49.99

This book will show Java developers how to use the Google Web Toolkit (GWT) to rapidly create rich web-based applications using their existing skills. It will cover the full development cycle, from ......一起来看看 《GWT in Action》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具