GoMock使用

安裝

安裝gomock軟件包和mockgen代碼生成工具

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

基本用法

1.用mockgen為要模擬的接口生成模擬绢陌。
2.在測試中創(chuàng)建一個(gè)市里gomock.Controller并將其傳遞給模擬對象構(gòu)造函數(shù)以獲取模擬對象筹裕。
3.調(diào)用EXPECT()為你的模擬設(shè)置他們的期望值和返回值
4.調(diào)用Finish()模擬控制器來斷言模擬的期望今阳。
5.舉個(gè)簡單的例子:

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

我們在模擬Doer時(shí)要的測試代碼

//位置: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的相對路徑占贫,否則會報(bào)錯(cuò)
  • Doer : 為此接口生成模擬同诫。這個(gè)參數(shù)是必需的 - 我們需要指定接口來顯式生成模擬。但是壮锻,我們 可以在此指定多個(gè)接口作為逗號分隔列表(例如 Doer1,Doer2)
    這樣在mocks下面就會自動生成一個(gè)mocks/mock_doer.go的文件琐旁,如下內(nèi)容:
// 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) - 避免此處的名稱沖突可能是非標(biāo)準(zhǔn)全大寫名稱的原因

接下來猜绣,我們 在測試中定義一個(gè) 模擬控制器灰殴。模擬控制器負(fù)責(zé)跟蹤和斷言其關(guān)聯(lián)模擬對象的期望。

我們可以通過將t 類型 的值傳遞*testing.T 給它的構(gòu)造函數(shù)來獲得模擬控制器 掰邢,然后使用它來構(gòu)造Doer 接口的模擬 牺陶。我們還有 defer 它的 Finish 方法。

假設(shè)我們要斷言 mockerDoer的 Do 方法將被調(diào)用 一次辣之,以 123 和 "Hello GoMock" 為參數(shù)掰伸,將返回 nil。

要做到這一點(diǎn)怀估,我們就可以調(diào)用 EXPECT() 上 mockDoer 建立其期望在我們的測試碱工。調(diào)用 EXPECT()返回一個(gè)對象(稱為模擬 記錄器),提供與真實(shí)對象相同名稱的方法奏夫。

調(diào)用模擬記錄器上的方法之一指定具有給定參數(shù)的預(yù)期調(diào)用怕篷。然后,您可以將其他屬性鏈接到呼叫上酗昼,例如:

返回值(通過 .Return(...))
此呼叫預(yù)計(jì)發(fā)生的次數(shù)(通過 .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()
}

最后廊谓,我們來運(yùn)行一下我們的測試:

$ 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 方法返回的值確實(shí)是由DoSomething返回的值。我們可以編寫另一個(gè)測試麻削,創(chuàng)建一個(gè)虛擬錯(cuò)誤蒸痹,然后將其指定為返回值

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 當(dāng)存在大量要模擬的接口/包時(shí),單獨(dú)運(yùn)行 每個(gè)包和接口是很麻煩的呛哟。為了緩解此問題叠荠, 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
}

注意:由于當(dāng)前的包doer的工作路徑是doer榛鼎,所以在destination要寫成 ../mocks/,而不是直接是mocks,執(zhí)行命令:

go generate ./...

那么我們現(xiàn)在可以運(yùn)行所有有g(shù)o:generate ./... 注釋的mocks者娱,注意:注釋符 //與go:generate之間沒有空格抡笼。

關(guān)于在何處放置go:generate 注釋以及要包括哪些接口的合理策略如下:

  • go:generate 每個(gè)文件一條注釋,包含要模擬的接口
  • 一調(diào)用mockgen黄鳍,就會為所有的接口生成mocks
  • 將模擬放入包中 mocks 并將文件的模擬 X.go 寫入 mocks/mock_X.go推姻。
    這樣, mockgen 調(diào)用接近實(shí)際接口框沟,同時(shí)避免了每個(gè)接口的單獨(dú)調(diào)用和目標(biāo)文件的開銷藏古。

使用參數(shù)匹配器

有時(shí),您不關(guān)心調(diào)用mock的特定參數(shù)忍燥。使用 GoMock拧晕,可以預(yù)期參數(shù)具有固定值(通過指定預(yù)期調(diào)用中的值),或者可以預(yù)期它與謂詞匹配灾前,稱為 匹配器防症。匹配器用于表示模擬方法的預(yù)期參數(shù)范圍孟辑。以下匹配器在GoMock中預(yù)定義 :
gomock.Any():匹配任何值(任何類型)
gomock.Eq(x):使用反射來匹配是值DeepEqual 到 x
gomock.Nil(): 火柴 nil
gomock.Not(m):( m 匹配器在哪里 )匹配匹配器不匹配的值 m
gomock.Not(x)(式中哎甲, x 是 不 一個(gè)Matcher)匹配的值不 DeepEqual 至 x

示例:
如果我們不關(guān)心第一個(gè)參數(shù)的值 Do,我們可以寫:

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

GoMock 自動將實(shí)際的參數(shù)轉(zhuǎn)換 Matcher 為 Eq 匹配器饲嗽,因此上述調(diào)用等效于:

mockDoer.EXPECT().DoSomething(gomock.Any(), gomock.Eq("Hello GoMock"))
您可以通過實(shí)現(xiàn)gomock.Matcher 界面來定義自己的匹配器 :
//位置:gomock/matchers.go
type Matcher interface {
    Matches(x interface{}) bool
    String() string
}

該 Matches 方法是實(shí)際匹配發(fā)生的地方炭玫,同時(shí) String 用于為失敗的測試生成人類可讀的輸出。例如貌虾,檢查參數(shù)類型的匹配器可以實(shí)現(xiàn)如下:

//位置: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中,我們必須 在一系列鏈?zhǔn)秸{(diào)用中將點(diǎn)放在每一行的 末尾

調(diào)用對象的順序通常很重要尽狠。 GoMock 提供了一種斷言一個(gè)調(diào)用必須在另一個(gè)調(diào)用之后發(fā)生的.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 還提供了一個(gè)便利功能践图, gomock.InOrder 用于指定必須按照給定的確切順序執(zhí)行調(diào)用。這比.After 直接使用靈活性要差 沉馆,但可以使您的測試對于更長的調(diào)用序列更具可讀性:

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"),
)
指定模擬操作

模擬對象與實(shí)際實(shí)現(xiàn)的不同之處在于它們不實(shí)現(xiàn)任何行為 - 它們所做的只是在適當(dāng)?shù)臅r(shí)刻提供預(yù)設(shè)響應(yīng)并記錄其調(diào)用码党。但是,有時(shí)你需要你的mock才能做更多的事情斥黑。在這里揖盘, GoMock的 Do 行動派上用場。任何調(diào)用都可以通過調(diào)用一個(gè)動作進(jìn)行修飾锌奴, .Do 每當(dāng)調(diào)用匹配時(shí)兽狭,都會執(zhí)行一個(gè)函數(shù):

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

關(guān)于調(diào)用參數(shù)的復(fù)雜斷言可以寫在 Do 操作中。例如,如果DoSomething第一個(gè)(int)參數(shù) 應(yīng)小于或等于second(string)參數(shù)的長度椭符,我們可以編寫:

mockDoer.EXPECT().
    DoSomething(gomock.Any(), gomock.Any()).
    Return(nil).
    Do(func(x int, y string) {
        if x > len(y) {
            t.Fail()
        }
    })
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末荔燎,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子销钝,更是在濱河造成了極大的恐慌有咨,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,423評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蒸健,死亡現(xiàn)場離奇詭異座享,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)似忧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,147評論 2 385
  • 文/潘曉璐 我一進(jìn)店門渣叛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人盯捌,你說我怎么就攤上這事淳衙。” “怎么了饺著?”我有些...
    開封第一講書人閱讀 157,019評論 0 348
  • 文/不壞的土叔 我叫張陵箫攀,是天一觀的道長。 經(jīng)常有香客問我幼衰,道長靴跛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,443評論 1 283
  • 正文 為了忘掉前任渡嚣,我火速辦了婚禮梢睛,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘识椰。我一直安慰自己绝葡,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,535評論 6 385
  • 文/花漫 我一把揭開白布腹鹉。 她就那樣靜靜地躺著藏畅,像睡著了一般。 火紅的嫁衣襯著肌膚如雪种蘸。 梳的紋絲不亂的頭發(fā)上墓赴,一...
    開封第一講書人閱讀 49,798評論 1 290
  • 那天,我揣著相機(jī)與錄音航瞭,去河邊找鬼诫硕。 笑死,一個(gè)胖子當(dāng)著我的面吹牛刊侯,可吹牛的內(nèi)容都是我干的章办。 我是一名探鬼主播,決...
    沈念sama閱讀 38,941評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼藕届!你這毒婦竟也來了挪蹭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,704評論 0 266
  • 序言:老撾萬榮一對情侶失蹤休偶,失蹤者是張志新(化名)和其女友劉穎梁厉,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體踏兜,經(jīng)...
    沈念sama閱讀 44,152評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡词顾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,494評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了碱妆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肉盹。...
    茶點(diǎn)故事閱讀 38,629評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖疹尾,靈堂內(nèi)的尸體忽然破棺而出上忍,到底是詐尸還是另有隱情,我是刑警寧澤纳本,帶...
    沈念sama閱讀 34,295評論 4 329
  • 正文 年R本政府宣布窍蓝,位于F島的核電站,受9級特大地震影響饮醇,放射性物質(zhì)發(fā)生泄漏它抱。R本人自食惡果不足惜秕豫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,901評論 3 313
  • 文/蒙蒙 一朴艰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧混移,春花似錦祠墅、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至回铛,卻和暖如春狗准,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背茵肃。 一陣腳步聲響...
    開封第一講書人閱讀 31,978評論 1 266
  • 我被黑心中介騙來泰國打工腔长, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人验残。 一個(gè)月前我還...
    沈念sama閱讀 46,333評論 2 360
  • 正文 我出身青樓捞附,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子鸟召,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,499評論 2 348

推薦閱讀更多精彩內(nèi)容