
在Golang的官方Repo(https://github.com/golang/)中有一個(gè)單獨(dú)的工程叫"mock"(https://github.com/golang/mock),雖然star不是特別多,但它卻是Golang官方放出來的mock工具,充這這點(diǎn)我們也需要使用下,雖然并不是官方的就是最好(比如比標(biāo)準(zhǔn)庫http更快的fasthttp)。
不同場(chǎng)景mock的對(duì)象互相不同,那么gomock主要是mock哪些內(nèi)容呢?
mockgen has two modes of operation: source and reflect. Source mode generates mock interfaces from a source file.
Reflect mode generates mock interfaces by building a program that uses reflection to understand interfaces.
通過gomock的輔助工具我們知道,gomock主要是針對(duì)我們go代碼中的接口進(jìn)行mock的。
安裝
gomock主要包含兩個(gè)部分:" gomock庫"和“ 輔助代碼生成工具mockgen”
他們都可以通過go get來獲?。?/p>
go get github.com/golang/mock/gomock
go get github.com/golang/mock/mockgen
如何你設(shè)置過$GOPATH/bin到你的$PATH變量中,那么這里就可以直接運(yùn)行mockgen命令了,否則需要使用絕對(duì)路徑或者相當(dāng)于$GOPATH的目錄。
示例
gomock的repo中帶了一個(gè)官方的例子,但是這個(gè)例子過于強(qiáng)大和豐富,反而不適合嘗鮮,下面我們寫個(gè)我們自己的例子(https://www.github.com/cz-it/blog/blog/Go/testing/gomock/example),一個(gè)獲取當(dāng)前Golang最新版本的例子:
tree .
.
├── go_version.go
├── main.go
└── spider
└── spider.go
目錄結(jié)構(gòu)如上。這里spider.go作為接口文件,定義了spider包的接口:
package spider
type Spider interface {
GetBody() string
}
這里假設(shè)接口GetBody直接可以抓取"https://golang.org"首頁的“Build version”字段來得到當(dāng)前Golang發(fā)布出來的版本。
這里在go_version.go中對(duì)這個(gè)接口進(jìn)行使用:
import (
"github.com/cz-it/blog/blog/Go/testing/gomock/example/spider"
)
func GetGoVersion(s spider.Spider) string {
body := s.GetBody()
return body
}
直接返回表示版本的字符串。正常情況下我們會(huì)寫出如下的單元測(cè)試代碼:
func TestGetGoVersion(t *testing.T) {
v := GetGoVersion(spider.CreateGoVersionSpider())
if v != "go1.8.3" {
t.Error("Get wrong version %s", v)
}
}
這里spider.CreateGoVersionSpider()返回一個(gè)實(shí)現(xiàn)了Spider接口的用來獲得Go版本號(hào)的爬蟲。
這個(gè)單元測(cè)試其實(shí)既測(cè)試了函數(shù)GetGoVersion也測(cè)試了spider.CreateGoVersionSpider返回的對(duì)象。
而有時(shí)候,我們可能僅僅想測(cè)試下GetGoVersion函數(shù),或者我們的spider.CreateGoVersionSpider爬蟲實(shí)現(xiàn)還沒有寫好,那該如何是好呢?
此時(shí)Mock工具就顯的尤為重要了。
這里首先用gomock提供的mockgen工具生成要mock的接口的實(shí)現(xiàn):
mockgen -destination spider/mock_spider.go -package spider github.com/cz-it/blog/blog/Go/testing/gomock/example/spider Spider
這里生成了文件:
└── spider
├── mock_spider.go
└── spider.go
這里注意的是,要預(yù)先創(chuàng)建好spider/mocks目錄。這樣我們的mock代碼就生成好了,在"spider/mocks/mock_spider.go"文件中。具體的內(nèi)容可以先不管。這里先看例子中怎么使用:
import (
"github.com/cz-it/blog/blog/Go/testing/gomock/example/spider"
"github.com/golang/mock/gomock"
"testing"
)
func TestGetGoVersion(t *testing.T) {
mockCtl := gomock.NewController(t)
mockSpider := spider.NewMockSpider(mockCtl)
mockSpider.EXPECT().GetBody().Return("go1.8.3")
goVer := GetGoVersion(mockSpider)
if goVer != "go1.8.3" {
t.Error("Get wrong version %s", goVer)
}
}
這里在單元測(cè)試中再也不用先去實(shí)現(xiàn)一個(gè)Spider接口了,而通過gomock為我們直接生成,然后再集成到我們的單元測(cè)試?yán)锩?。可以看到gomock和testing單元測(cè)試框架可以緊密的結(jié)合起來工作。
mockgen工具
在生成mock代碼的時(shí)候,我們用到了mockgen工具,這個(gè)工具是gomock提供的用來為要mock的接口生成實(shí)現(xiàn)的。它可以根據(jù)給定的接口,來自動(dòng)生成代碼。這里給定接口有兩種方式:接口文件和實(shí)現(xiàn)文件
接口文件
如果有接口文件,則可以通過:
- -source: 指定接口文件
- -destination: 生成的文件名
- -package:生成文件的包名
- -imports: 依賴的需要import的包
- -aux_files:接口文件不止一個(gè)文件時(shí)附加文件
- -build_flags: 傳遞給build工具的參數(shù)
比如mock代碼使用
mockgen -destination spider/mock_spider.go -package spider -source spider/spider.go
就是將接口spider/spider.go中的接口做實(shí)現(xiàn)并存在 spider/mock_spider.go文件中,文件的包名為"spider"。
實(shí)現(xiàn)文件
在我們的上面的例子中,并沒有使用"-source",那是如何實(shí)現(xiàn)接口的呢?mockgen還支持通過反射的方式來找到對(duì)應(yīng)的接口。只要在所有選項(xiàng)的最后增加一個(gè)包名和里面對(duì)應(yīng)的類型就可以了。其他參數(shù)和上面的公用。
通過注釋指定mockgen
如上所述,如果有多個(gè)文件,并且分散在不同的位置,那么我們要生成mock文件的時(shí)候,需要對(duì)每個(gè)文件執(zhí)行多次mockgen命令(假設(shè)包名不相同)。這樣在真正操作起來的時(shí)候非常繁瑣,mockgen還提供了一種通過注釋生成mock文件的方式,此時(shí)需要借助go的"go generate "工具。
在接口文件的注釋里面增加如下:
//go:generate mockgen -destination mock_spider.go -package spider github.com/cz-it/blog/blog/Go/testing/gomock/example/spider Spider
這樣,只要在spider目錄下執(zhí)行
go generate
命令就可以自動(dòng)生成mock文件了。
gomock的接口使用
在生成了mock實(shí)現(xiàn)代碼之后,我們就可以進(jìn)行正常使用了。這里假設(shè)結(jié)合testing進(jìn)行使用(當(dāng)然你也可考慮使用GoConvey)。我們就可以
在單元測(cè)試代碼里面首先創(chuàng)建一個(gè)mock控制器:
mockCtl := gomock.NewController(t)
將* testing.T傳遞給gomock生成一個(gè)"Controller"對(duì)象,該對(duì)象控制了整個(gè)Mock的過程。在操作完后還需要進(jìn)行回收,所以一般會(huì)在New后面defer一個(gè)Finish
defer mockCtl.Finish()
然后就是調(diào)用mock生成代碼里面為我們實(shí)現(xiàn)的接口對(duì)象:
mockSpider := spider.NewMockSpider(mockCtl)
這里的"spider"是mockgen命令里面?zhèn)鬟f的報(bào)名,后面是NewMockXxxx格式的對(duì)象創(chuàng)建函數(shù)"Xxx"是接口名。這里需要傳遞控制器對(duì)象進(jìn)去。返回一個(gè)接口的實(shí)現(xiàn)對(duì)象。
有了實(shí)現(xiàn)對(duì)象,我們就可以調(diào)用其斷言方法了:EXPECT()
這里gomock非常牛的采用了鏈?zhǔn)秸{(diào)用法,和Swfit以及ObjectiveC里面的Masonry庫一樣,通過"."連接函數(shù)調(diào)用,可以像鏈條一樣連接下去。
mockSpider.EXPECT().GetBody().Return("go1.8.3")
這里的每個(gè)"."調(diào)用都得到一個(gè)"Call"對(duì)象,該對(duì)象有如下方法:
func (c *Call) After(preReq *Call) *Call
func (c *Call) AnyTimes() *Call
func (c *Call) Do(f interface{}) *Call
func (c *Call) MaxTimes(n int) *Call
func (c *Call) MinTimes(n int) *Call
func (c *Call) Return(rets ...interface{}) *Call
func (c *Call) SetArg(n int, value interface{}) *Call
func (c *Call) String() string
func (c *Call) Times(n int) *Call
這里EXPECT()得到實(shí)現(xiàn)的對(duì)象,然后調(diào)用實(shí)現(xiàn)對(duì)象的接口方法,接口方法返回第一個(gè)"Call"對(duì)象,
然后對(duì)其進(jìn)行條件約束。
上面約束都可以在文檔中或者根據(jù)字面意思進(jìn)行理解,這里列舉幾個(gè)例子:
指定返回值
如我們的例子,調(diào)用Call的Return函數(shù),可以指定接口的返回值:
mockSpider.EXPECT().GetBody().Return("go1.8.3")
這里我們指定返回接口函數(shù)GetBody()返回"go1.8.3"。
指定執(zhí)行次數(shù)
有時(shí)候我們需要指定函數(shù)執(zhí)行多次,比如接受網(wǎng)絡(luò)請(qǐng)求的函數(shù),計(jì)算其執(zhí)行了多少次。
mockSpider.EXPECT().Recv().Return(nil).Times(3)
執(zhí)行三次Recv函數(shù),這里還可以有另外幾種限制:
- AnyTimes() : 0到多次
- MaxTimes(n int) :最多執(zhí)行n次,如果沒有設(shè)置
- MinTimes(n int) :最少執(zhí)行n次,如果沒有設(shè)置
指定執(zhí)行順序
有時(shí)候我們還要指定執(zhí)行順序,比如要先執(zhí)行Init操作,然后才能執(zhí)行Recv操作。
initCall := mockSpider.EXPECT().Init()
mockSpider.EXPECT().Recv().After(initCall)
再來回望官方Sample
Sample的結(jié)構(gòu)如下:
sample/
├── README.md
├── imp1
│ └── imp1.go
├── imp2
│ └── imp2.go
├── imp3
│ └── imp3.go
├── imp4
│ └── imp4.go
├── mock_user
│ └── mock_user.go
├── user.go
└── user_test.go
這里,user.go是包含要mock的接口函數(shù)的目標(biāo)文件,而imp1-4是user.go里面接口依賴的文件用來模擬"-imports"和"-aux_files"選項(xiàng)。
user_test.go 文件如同我們的test文件,是對(duì)gomock的調(diào)用。
而mock_user是生成mock文件的目錄。里面的mock_user.go是通過mockgen生成的。
這里我們看到user.go有g(shù)enerate的注釋:
//go:generate mockgen -destination mock_user/mock_user.go github.com/golang/mock/sample Index,Embed,Embedded
這里指定了同一個(gè)包里面的三個(gè)接口。然后定義了三個(gè)接口,里面方法有依賴impx四個(gè)目錄中的文件:
type Embed interface {
...
}
type Embedded interface {
...
}
type Index interface {
...
ForeignOne(imp1.Imp1)
ForeignTwo(renamed2.Imp2)
ForeignThree(Imp3)
ForeignFour(imp_four.Imp4)
...
}
以及其他函數(shù)。
最后來看調(diào)用,在user_test.go中首先創(chuàng)建控制器并調(diào)用其Finish函數(shù):
ctrl := gomock.NewController(t)
defer ctrl.Finish()
然后就是如上面我介紹的,這里分開在幾個(gè)不同Test函數(shù)中,流程基本上,依次創(chuàng)建mock對(duì)象:
mockIndex := mock_user.NewMockIndex(ctrl)
然后調(diào)用其mock的方法:
mockIndex.EXPECT().Put("a", 1)
boolc := make(chan bool)
mockIndex.EXPECT().ConcreteRet().Return(boolc)
最后運(yùn)行go test就可以進(jìn)行測(cè)試了。
$ go test
PASS
ok github.com/golang/mock/sample 0.013s