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

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

原文地址:http://blog.51cto.com/9291927/2346777

时间: 2024-10-10 03:41:56

Go语言开发(二十一)、GoMock测试框架的相关文章

Go语言开发(十一)、Go语言常用标准库一

Go语言开发(十一).Go语言常用标准库一 一.log 1.log模块简介 Go语言中log模块用于在程序中输出日志.log模块提供了三类日志输出接口,Print.Fatal和Panic.Print是普通输出:Fatal是在执行完Print后,执行 os.Exit(1):Panic是在执行完Print后调用panic()方法.log模块对每一类接口其提供了3中调用方式,分别是"Xxxx. Xxxxln.Xxxxf". 2.log.Print接口 log.Print类接口包括log.Pr

测试驱动开发TDD(二)开源测试框架CppUnit

背景 CppUnit 是个基于 LGPL 的开源项目,最初版本移植自 JUnit,是一个非常优秀的开源测试框架.CppUnit 和 JUnit 一样主要思想来源于极限编程(XProgramming).主要功能就是对单元测试进行管理,并可进行自动化测试.这样描述可能没有让您体会到测试框架的强大威力,那您在开发过程中遇到下列问题吗?如果答案是肯定的,就应该学习使用这种技术: 测试代码没有很好地维护而废弃,再次需要测试时还需要重写: 投入太多的精力,找 bug,而新的代码仍然会出现类似 bug: 写完

前端开发--实战篇之测试框架

如果不了解前端开发环境,请参考搭建前端开发环境, 如果不了解实战篇的项目配置,请参考前端开发--实战篇 步骤一:待命 在cmd里面,进入到public文件夹待命. 步骤二:初始化karma配置文件karma.conf.js 执行初始化配置文件的命令: karma init 根据向导,大多数使用默认配置即可.具体见下图: 步骤三:根据当前目录结构,修改配置文件karma.conf.js 添加待测试的js文件: files: [   'app/dist/lib/angular/*.js',   'a

python运维开发(二十一)----文件上传和验证码+session

内容目录: 文件上传 HTML Form表单提交 ajax提交 原生ajax提交 jQuery Ajax提交 验证码+session 文件和图片的上传功能

Pyunit测试框架

一.概述 本系列主要解决的问题是“接口自动化测试”,选择的测试语言是 python 脚本语言.截至目前为止,python是公认的最好的用于自动化应用的语言之一 二.PyUnit测试框架 使用 python 作为自动化编程语言,那么就自然的使用 pyunit 作为自动化测试框架了.测试框架unittest要达到如下目标: ● 支持自动化测试 ● 让所有的测试脚本共享 开启(setup) 和 关闭(shutdown) 的代码 ● 可以通过集合(collections)的方式来组织测试用例脚本 ● 将

Java Junit测试框架

Java    Junit测试框架 1.相关概念 ? JUnit:是一个开发源代码的Java测试框架,用于编写和运行可重复的测试.它是用于单元测试框架体系xUnit的一个实例(用于java语言).主要用于白盒测试,回归测试. ? 白盒测试:把测试对象看作一个打开的盒子,程序内部的逻辑结构和其他信息对测试人 员是公开的. ? 回归测试:软件或环境的修复或更正后的再测试,自动测试工具对这类测试尤其有用. ? 单元测试:最小粒度的测试,以测试某个功能或代码块.一般由程序员来做,因为它需要知道内部程序设

前端测试框架

一.为什么要进行测试? 一个 bug 被隐藏的时间越长,修复这个 bug 的代价就越大.大量的研究数据指出:最后才修改一个 bug 的代价是在 bug 产生时修改它的代价的10倍.所以要防患于未然. 从语言的角度讲 JavaScript 作为 web 端使用最广泛的编程语言,它是动态语言,缺乏静态类型检查,所以在代码编译期间,很难发现像变量名写错,调用不存在的方法, 赋值或传值的类型错误等错误. 例如下面的例子, 这种类型不符的情况在代码中非常容易发生 function foo(x) { ret

Java高级特性 第11节 JUnit 3.x和JUnit 4.x测试框架

一.软件测试 1.软件测试的概念及分类 软件测试是使用人工或者自动手段来运行或测试某个系统的过程,其目的在于检验它是否满足规定的需求或弄清预期结果与实际结果之间的差别.它是帮助识别开发完成(中间或最终的版本)的计算机软件(整体或部分)的正确度 .完全度和质量的软件过程. 软件测试过程: 2.软件测试的分类 按是否关心软件内部结构和具体实现角度来分: 黑盒测试(Black-box Testing) 黑盒测试也称功能测试,测试中把被测的软件当成一个黑盒子,不关心盒子的内部结构是什么,只关心软件的输入

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

Go语言开发(二十).GoStub测试框架 一.GoStub简介 GoStub是一款轻量级的单元测试框架,接口友好,可以对全局变量.函数或过程进行打桩.GoStub安装:go get github.com/prashantv/gostub 二.GoStub常用方法 gostub用于在测试时打桩变量,一旦测试运行时,重置原来的值. type Stubs struct { // stubs is a map from the variable pointer (being stubbed) to t