GoMock使用

安装

安装gomock软件包和mockgen代码生成工具

go get github.com/golang/mock/gomock 
go github.com/golang/mock/mockgen

基本用法

1.用mockgen为要模拟的接口生成模拟。
2.在测试中创建一个市里gomock.Controller并将其传递给模拟对象构造函数以获取模拟对象。
3.调用EXPECT()为你的模拟设置他们的期望值和返回值
4.调用Finish()模拟控制器来断言模拟的期望。
5.举个简单的例子:

//位置:doer/doer.go
package doer
type Doer interface {
    DoSomething(int, string) error
}

我们在模拟Doer时要的测试代码

//位置:user/user.go
package user

import "cold/stu1/doer"

type User struct {
    Doer doer.Doer
}

func (u *User) Use() error {
    return u.Doer.DoSomething(123, "Hello Gomock")
}

生成Doer的模拟代码:

mkdir -p mocks

mockgen -destination=mocks/mock_doer.go -package=mocks cold/stu1/doer Doer

注释:

  • -destination=mocks/mock_doer.go : 将生成的模拟接口放入文件中mocks/mock_doer.go
  • -package=mocks : 将生成的模拟接口放入包mocks中
  • cold/stu1/doer:为此包生成模拟,注意:这里不要加入绝对路径,加入$GOPATH的相对路径,否则会报错
  • Doer : 为此接口生成模拟。这个参数是必需的 - 我们需要指定接口来显式生成模拟。但是,我们 可以在此指定多个接口作为逗号分隔列表(例如 Doer1,Doer2)
    这样在mocks下面就会自动生成一个mocks/mock_doer.go的文件,如下内容:
// Code generated by MockGen. DO NOT EDIT.
// Source: cold/stu1/doer (interfaces: Doer)

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

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

// MockDoer is a mock of Doer interface
type MockDoer struct {
    ctrl     *gomock.Controller
    recorder *MockDoerMockRecorder
}

// MockDoerMockRecorder is the mock recorder for MockDoer
type MockDoerMockRecorder struct {
    mock *MockDoer
}

// NewMockDoer creates a new mock instance
func NewMockDoer(ctrl *gomock.Controller) *MockDoer {
    mock := &MockDoer{ctrl: ctrl}
    mock.recorder = &MockDoerMockRecorder{mock}
    return mock
}

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

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

// DoSomething indicates an expected call of DoSomething
func (mr *MockDoerMockRecorder) DoSomething(arg0, arg1 interface{}) *gomock.Call {
    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoSomething", reflect.TypeOf((*MockDoer)(nil).DoSomething), arg0, arg1)
}

请注意,生成的 EXPECT() 方法在与mock方法相同的对象上定义(在本例中 DoSomething) - 避免此处的名称冲突可能是非标准全大写名称的原因

接下来,我们 在测试中定义一个 模拟控制器。模拟控制器负责跟踪和断言其关联模拟对象的期望。

我们可以通过将t 类型 的值传递*testing.T 给它的构造函数来获得模拟控制器 ,然后使用它来构造Doer 接口的模拟 。我们还有 defer 它的 Finish 方法。

假设我们要断言 mockerDoer的 Do 方法将被调用 一次,以 123 和 "Hello GoMock" 为参数,将返回 nil。

要做到这一点,我们就可以调用 EXPECT() 上 mockDoer 建立其期望在我们的测试。调用 EXPECT()返回一个对象(称为模拟 记录器),提供与真实对象相同名称的方法。

调用模拟记录器上的方法之一指定具有给定参数的预期调用。然后,您可以将其他属性链接到呼叫上,例如:

返回值(通过 .Return(...))
此呼叫预计发生的次数(通过 .Times(number)或通过 .MaxTimes(number) 和 .MinTimes(number))

代码如下:


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

    mockDoer := mocks.NewMockDoer(mockCtrl)
    testUser := &user.User{Doer: mockDoer}

    mockDoer.EXPECT().DoSomething(123, "Hello Gomock").Return(nil).Times(1)

    testUser.Use()
}

最后,我们来运行一下我们的测试:

$ go test -v cold/stu1/user
=== RUN TestUse
--- PASS: TestUse (0.00s)
=== RUN TestUseReturnsErrorFromDo
--- PASS: TestUseReturnsErrorFromDo (0.00s)
PASS
ok cold/stu1/user 0.002s

我们可能还想声明Use 方法返回的值确实是由DoSomething返回的值。我们可以编写另一个测试,创建一个虚拟错误,然后将其指定为返回值

func TestUseReturnsErrorFromDo(t *testing.T) {
    mockCtr := gomock.NewController(t)
    defer mockCtr.Finish()

    dummyError := errors.New("dummy error")
    mockDoer := mocks.NewMockDoer(mockCtr)
    testUser := &user.User{Doer: mockDoer}

    mockDoer.EXPECT().DoSomething(123, "Hello Gomock").Return(dummyError).Times(1)

    err := testUser.Use()

    if err != dummyError {
        t.Fail()
    }
}

使用GoMock 的 go:generate

mockgen 当存在大量要模拟的接口/包时,单独运行 每个包和接口是很麻烦的。为了缓解此问题, mockgen 可以将命令放在特殊 go:generate 注释中
在我们的示例中,我们可以在我们go:generate 的package 声明 下方添加 注释 doer.go:

package doer

//go:generate mockgen -destination=../mocks/mock_doer.go -package=mocks cold/stu1/doer Doer

type Doer interface {
    DoSomething(int, string) error
}

注意:由于当前的包doer的工作路径是doer,所以在destination要写成 ../mocks/,而不是直接是mocks,执行命令:

go generate ./...

那么我们现在可以运行所有有go:generate ./... 注释的mocks,注意:注释符 //与go:generate之间没有空格。

关于在何处放置go:generate 注释以及要包括哪些接口的合理策略如下:

  • go:generate 每个文件一条注释,包含要模拟的接口
  • 一调用mockgen,就会为所有的接口生成mocks
  • 将模拟放入包中 mocks 并将文件的模拟 X.go 写入 mocks/mock_X.go。
    这样, mockgen 调用接近实际接口,同时避免了每个接口的单独调用和目标文件的开销。

使用参数匹配器

有时,您不关心调用mock的特定参数。使用 GoMock,可以预期参数具有固定值(通过指定预期调用中的值),或者可以预期它与谓词匹配,称为 匹配器。匹配器用于表示模拟方法的预期参数范围。以下匹配器在GoMock中预定义 :
gomock.Any():匹配任何值(任何类型)
gomock.Eq(x):使用反射来匹配是值DeepEqual 到 x
gomock.Nil(): 火柴 nil
gomock.Not(m):( m 匹配器在哪里 )匹配匹配器不匹配的值 m
gomock.Not(x)(式中, x 是 不 一个Matcher)匹配的值不 DeepEqual 至 x

示例:
如果我们不关心第一个参数的值 Do,我们可以写:

mockDoer.EXPECT().DoSomething(gomock.Any(), "Hello GoMock")

GoMock 自动将实际的参数转换 Matcher 为 Eq 匹配器,因此上述调用等效于:

mockDoer.EXPECT().DoSomething(gomock.Any(), gomock.Eq("Hello GoMock"))
您可以通过实现gomock.Matcher 界面来定义自己的匹配器 :
//位置:gomock/matchers.go
type Matcher interface {
    Matches(x interface{}) bool
    String() string
}

该 Matches 方法是实际匹配发生的地方,同时 String 用于为失败的测试生成人类可读的输出。例如,检查参数类型的匹配器可以实现如下:

//位置:match/oftype.go
package match

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

type ofType struct{ t string }

func OfType(t string) gomock.Matcher {
    return &ofType{t}
}

func (o *ofType) Matches(x interface{}) bool {
    return reflect.TypeOf(x).String() == o.t
}

func (o *ofType) String() string {
    return "is of type " + o.t
}

我们可是使用自定义的matcher如下:

// Expect Do to be called once with 123 and any string as parameters, and return nil from the mocked call.
mockDoer.EXPECT().
   DoSomething(123, match.OfType("string")).
   Return(nil).
   Times(1)

请注意,在Go中,我们必须 在一系列链式调用中将点放在每一行的 末尾

调用对象的顺序通常很重要。 GoMock 提供了一种断言一个调用必须在另一个调用之后发生的.After 方法,即 方法。例如,

callFirst := mockDoer.EXPECT().DoSomething(1, "first this")
callA := mockDoer.EXPECT().DoSomething(2, "then this").After(callFirst)
callB := mockDoer.EXPECT().DoSomething(2, "or this").After(callFirst)

GoMock 还提供了一个便利功能, gomock.InOrder 用于指定必须按照给定的确切顺序执行调用。这比.After 直接使用灵活性要差 ,但可以使您的测试对于更长的调用序列更具可读性:

gomock.InOrder(
    mockDoer.EXPECT().DoSomething(1, "first this"),
    mockDoer.EXPECT().DoSomething(2, "then this"),
    mockDoer.EXPECT().DoSomething(3, "then this"),
    mockDoer.EXPECT().DoSomething(4, "finally this"),
)
指定模拟操作

模拟对象与实际实现的不同之处在于它们不实现任何行为 - 它们所做的只是在适当的时刻提供预设响应并记录其调用。但是,有时你需要你的mock才能做更多的事情。在这里, GoMock的 Do 行动派上用场。任何调用都可以通过调用一个动作进行修饰, .Do 每当调用匹配时,都会执行一个函数:

mockDoer.EXPECT().
    DoSomething(gomock.Any(), gomock.Any()).
    Return(nil).
    Do(func(x int, y string) {
        fmt.Println("Called with x =",x,"and y =", y)
    })

关于调用参数的复杂断言可以写在 Do 操作中。例如,如果DoSomething第一个(int)参数 应小于或等于second(string)参数的长度,我们可以编写:

mockDoer.EXPECT().
    DoSomething(gomock.Any(), gomock.Any()).
    Return(nil).
    Do(func(x int, y string) {
        if x > len(y) {
            t.Fail()
        }
    })
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容