這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
在Golang的官方Repo(https://github.com/golang/)中有一個單獨的工程叫"mock"(https://github.com/golang/mock),雖然star不是特別多,但它卻是Golang官方放出來的mock工具,充這這點我們也需要使用下,雖然並不是官方的就是最好(比如比標準庫http更快的fasthttp)。
不同情境mock的對象互相不同,那麼gomock主要是mock哪些內容呢?
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主要是針對我們go代碼中的介面進行mock的。
安裝
gomock主要包含兩個部分:" gomock庫"和“ 輔助代碼產生工具mockgen”
他們都可以通過go get
來擷取:
go get github.com/golang/mock/gomockgo get github.com/golang/mock/mockgen
如何你設定過$GOPATH/bin到你的$PATH變數中,那麼這裡就可以直接運行mockgen命令了,否則需要使用絕對路徑或者相當於$GOPATH的目錄。
樣本
gomock的repo中帶了一個官方的例子,但是這個例子過於強大和豐富,反而不適合嘗鮮,下面我們寫個我們自己的例子(https://www.github.com/cz-it/blog/blog/Go/testing/gomock/example),一個擷取當前Golang最新版本的例子:
tree ..├── go_version.go├── main.go└── spider └── spider.go
目錄結構如上。這裡spider.go作為介面檔案,定義了spider包的介面:
package spidertype Spider interface { GetBody() string}
這裡假設介面GetBody
直接可以抓取"https://golang.org"首頁的“Build version”欄位來得到當前Golang發布出來的版本。
這裡在go_version.go中對這個介面進行使用:
import ( "github.com/cz-it/blog/blog/Go/testing/gomock/example/spider")func GetGoVersion(s spider.Spider) string { body := s.GetBody() return body}
直接返回表示版本的字串。正常情況下我們會寫出如下的單元測試代碼:
func TestGetGoVersion(t *testing.T) { v := GetGoVersion(spider.CreateGoVersionSpider()) if v != "go1.8.3" { t.Error("Get wrong version %s", v) }}
這裡spider.CreateGoVersionSpider()
返回一個實現了Spider
介面的用來獲得Go版本號碼的爬蟲。
這個單元測試其實既測試了函數GetGoVersion也測試了spider.CreateGoVersionSpider
返回的對象。
而有時候,我們可能僅僅想測試下GetGoVersion
函數,或者我們的spider.CreateGoVersionSpider
爬蟲實現還沒有寫好,那該如何是好呢?
此時Mock工具就顯的尤為重要了。
這裡首先用gomock提供的mockgen工具產生要mock的介面的實現:
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
這裡注意的是,要預先建立好spider/mocks目錄。這樣我們的mock代碼就產生好了,在"spider/mocks/mock_spider.go"檔案中。具體的內容可以先不管。這裡先看例子中怎麼使用:
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) }}
這裡在單元測試中再也不用先去實現一個Spider
介面了,而通過gomock為我們直接產生,然後再整合到我們的單元測試裡面。可以看到gomock和testing單元測試架構可以緊密的結合起來工作。
mockgen工具
在產生mock代碼的時候,我們用到了mockgen工具,這個工具是gomock提供的用來為要mock的介面產生實現的。它可以根據給定的介面,來自動產生代碼。這裡給定介面有兩種方式:介面檔案和實現檔案
介面檔案
如果有介面檔案,則可以通過:
- -source: 指定介面檔案
- -destination: 產生的檔案名稱
- -package:組建檔案的包名
- -imports: 依賴的需要import的包
- -aux_files:介面檔案不止一個檔案時附加檔案
- -build_flags: 傳遞給build工具的參數
比如mock代碼使用
mockgen -destination spider/mock_spider.go -package spider -source spider/spider.go
就是將介面spider/spider.go中的介面做實現並存在 spider/mock_spider.go檔案中,檔案的包名為"spider"。
實現檔案
在我們的上面的例子中,並沒有使用"-source",那是如何?介面的呢?mockgen還支援通過反射的方式來找到對應的介面。只要在所有選項的最後增加一個包名和裡面對應的類型就可以了。其他參數和上面的公用。
通過注釋指定mockgen
如上所述,如果有多個檔案,並且分散在不同的位置,那麼我們要產生mock檔案的時候,需要對每個檔案執行多次mockgen命令(假設包名不相同)。這樣在真正操作起來的時候非常繁瑣,mockgen還提供了一種通過注釋產生mock檔案的方式,此時需要藉助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目錄下執行
go generate
命令就可以自動產生mock檔案了。
gomock的介面使用
在產生了mock實現代碼之後,我們就可以進行正常使用了。這裡假設結合testing進行使用(當然你也可考慮使用GoConvey)。我們就可以
在單元測試代碼裡面首先建立一個mock控制器:
mockCtl := gomock.NewController(t)
將* testing.T
傳遞給gomock產生一個"Controller"對象,該對象控制了整個Mock的過程。在操作完後還需要進行回收,所以一般會在New後面defer一個Finish
defer mockCtl.Finish()
然後就是調用mock產生代碼裡面為我們實現的介面對象:
mockSpider := spider.NewMockSpider(mockCtl)
這裡的"spider"是mockgen命令裡面傳遞的報名,後面是NewMockXxxx
格式的對象建立函數"Xxx"是介面名。這裡需要傳遞控制器對象進去。返回一個介面的實現對象。
有了實現對象,我們就可以調用其斷言方法了:EXPECT()
這裡gomock非常牛的採用了鏈式調用法,和Swfit以及ObjectiveC裡面的Masonry庫一樣,通過"."串連函數調用,可以像鏈條一樣串連下去。
mockSpider.EXPECT().GetBody().Return("go1.8.3")
這裡的每個"."調用都得到一個"Call"對象,該對象有如下方法:
func (c *Call) After(preReq *Call) *Callfunc (c *Call) AnyTimes() *Callfunc (c *Call) Do(f interface{}) *Callfunc (c *Call) MaxTimes(n int) *Callfunc (c *Call) MinTimes(n int) *Callfunc (c *Call) Return(rets ...interface{}) *Callfunc (c *Call) SetArg(n int, value interface{}) *Callfunc (c *Call) String() stringfunc (c *Call) Times(n int) *Call
這裡EXPECT()
得到實現的對象,然後調用實現對象的介面方法,介面方法返回第一個"Call"對象,
然後對其進行條件約束。
上面約束都可以在文檔中或者根據字面意思進行理解,這裡列舉幾個例子:
指定傳回值
如我們的例子,調用Call的Return
函數,可以指定介面的傳回值:
mockSpider.EXPECT().GetBody().Return("go1.8.3")
這裡我們指定返回介面函數GetBody()
返回"go1.8.3"。
指定執行次數
有時候我們需要指定函數執行多次,比如接受網路請求的函數,計算其執行了多少次。
mockSpider.EXPECT().Recv().Return(nil).Times(3)
執行三次Recv函數,這裡還可以有另外幾種限制:
- AnyTimes() : 0到多次
- MaxTimes(n int) :最多執行n次,如果沒有設定
- MinTimes(n int) :最少執行n次,如果沒有設定
指定執行順序
有時候我們還要指定執行順序,比如要先執行Init操作,然後才能執行Recv操作。
initCall := mockSpider.EXPECT().Init()mockSpider.EXPECT().Recv().After(initCall)
再來回望官方Sample
Sample的結構如下:
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的介面函數的目標檔案,而imp1-4是user.go裡面介面依賴的檔案用來類比"-imports"和"-aux_files"選項。
user_test.go 檔案如同我們的test檔案,是對gomock的調用。
而mock_user是產生mock檔案的目錄。裡面的mock_user.go是通過mockgen產生的。
這裡我們看到user.go有generate的注釋:
//go:generate mockgen -destination mock_user/mock_user.go github.com/golang/mock/sample Index,Embed,Embedded
這裡指定了同一個包裡面的三個介面。然後定義了三個介面,裡面方法有依賴impx四個目錄中的檔案:
type Embed interface { ...}type Embedded interface { ...}type Index interface { ... ForeignOne(imp1.Imp1) ForeignTwo(renamed2.Imp2) ForeignThree(Imp3) ForeignFour(imp_four.Imp4) ...}
以及其他函數。
最後來看調用,在user_test.go中首先建立控制器並調用其Finish函數:
ctrl := gomock.NewController(t)defer ctrl.Finish()
然後就是如上面我介紹的,這裡分開在幾個不同Test函數中,流程基本上,依次建立mock對象:
mockIndex := mock_user.NewMockIndex(ctrl)
然後調用其mock的方法:
mockIndex.EXPECT().Put("a", 1)boolc := make(chan bool)mockIndex.EXPECT().ConcreteRet().Return(boolc)
最後運行go test
就可以進行測試了。
$ go testPASSok github.com/golang/mock/sample 0.013s