安裝
安裝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()
}
})