2020年12月16日 星期三

[ 文章收集 ] 使用 Golang 的官方 mock 工具 - gomock

 Source From Here

Preface
在 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/gomock
# go get github.com/golang/mock/mockgen

假設你已經設置過 $GOPATH/bin 到你的 $PATH 變量中,那麼這裡就可以直接運行 mockgen 命令了,否則需要使用絕對路徑或者相當於 $GOPATH 的目錄。

範例
gomock 的 repo中帶了一個 官方的例子,但是這個例子過於強大和豐富,反而不適合嚐鮮,下面我們寫個我們自己的例子, 一個獲取當前Golang最新版本的例子:
  1. root@localhost:src# tree mock_ex/  
  2. mock_ex/  
  3. ├── main.go  
  4. ├── mock_ex  
  5. ├── mocks  
  6. │   └── mock_spider.go  
  7. ├── spider  
  8. │   ├── mocks  
  9. │   │   └── mock_spider.go  
  10. │   └── spider.go  
  11. └── utils  
  12.     ├── go_version.go  
  13.     ├── go_version_test.go  
  14.     ├── mock_ex  
  15.     └── my_lib.go  
  16.   
  17. 4 directories, 9 files  
目錄結構如上。這裡 spider.go 作為接口文件,定義了 spider 包的接口:
spider.go
  1. package spider  
  2.   
  3. type Spider interface {  
  4.     GetBody() string  
  5. }  
這裡假設接口 GetBody 直接可以抓取 "https://golang.org" 首頁的 “Build version” 字段來得到當前 Golang 發佈出來的版本。這裡在 go_version.go 中對這個接口進行使用:
  1. package utils  
  2.   
  3. import (  
  4.     "mock_ex/spider"  
  5. )  
  6.   
  7. func GetGoVersion(s spider.Spider) string {  
  8.     show()  
  9.     body := s.GetBody()  
  10.     return body  
  11. }  
直接返回表示版本的字符串。正常情況下我們會寫出如下的單元測試代碼:
  1. func TestGetGoVersion(t *testing.T) {  
  2.     v := GetGoVersion(spider.CreateGoVersionSpider())  
  3.     if v != "go1.8.3" {  
  4.         t.Error("Get wrong version %s", v)  
  5.     }  
  6. }  
這裡 spider.CreateGoVersionSpider() 返回一個實現了 Spider 接口的用來獲得 Go 版本號的爬蟲。這個單元測試其實既測試了函數 GetGoVersion 也測試了 spider.CreateGoVersionSpider 返回的對象. 而有時候,我們可能僅僅想測試下 GetGoVersion 函數,或者我們的 spider.CreateGoVersionSpider 爬蟲實現還沒有寫好,那該如何是好呢?

此時 Mock 工具就顯的尤為重要了。這里首先用 gomock 提供的 mockgen 工俱生成要 mock 的接口的實現:
# mkdir mocks
# mockgen -destination=mocks/mock_spider.go -package=mocks mock_ex/spider Spider
# cat spider/mock_spider.go

這裡生成了文件:
  1. # tree mocks/  
  2. mocks/  
  3. └── mock_spider.go  
在 "mocks/mock_spider.go" 文件中。具體的內容可以先不管。這裡先看例子中怎麼使用:
utils/go_version_test.go
  1. package utils_test  
  2.   
  3. import (  
  4.     "fmt"  
  5.     "mock_ex/mocks"  
  6.     "mock_ex/utils"  
  7.     "github.com/golang/mock/gomock"  
  8.     "testing"  
  9. )  
  10.   
  11. func TestGetGoVersion(t *testing.T) {  
  12.     mockCtl := gomock.NewController(t)  
  13.     mockSpider := mocks.NewMockSpider(mockCtl)  
  14.     mockSpider.EXPECT().GetBody().Return("go1.8.3")  
  15.   
  16.     goVer := utils.GetGoVersion(mockSpider)  
  17.     if goVer != "go1.8.3" {  
  18.         t.Error(fmt.Sprintf("Get wrong version %s", goVer))  
  19.     }  
  20. }  
這裡在單元測試中再也不用先去實現一個Spider接口了,而通過 gomock 為我們直接生成,然後再集成到我們的單元測試裡面。可以看到 gomock 和 testing 單元測試框架可以緊密的結合起來工作。

mockgen 工具
在生成 mock 代碼的時候,我們用到了 mockgen 工具,這個工具是 gomock 提供的用來為要 mock 的接口生成實現的。它可以根據給定的接口,來自動生成代碼。這裡給定接口有兩種方式:接口文件和實現文件

接口文件
如果有接口文件,則可以通過:
-source: 指定接口文件
-destination: 生成的文件名
-package:生成文件的包名
-imports: 依賴的需要import的包
-aux_files:接口文件不止一個文件時附加文件
-build_flags: 傳遞給build工具的參數

一個使用範例如下:
# mockgen -destination=mocks/mock_spider.go -package=mocks mock_ex/spider Spider

就是將檔案 spider/spider.go 中的介面 Spider (多個介面名稱使用 "," 分開) 做實現並存在 mocks/mock_spider.go 文件中,文件的套件名為 "mocks"。

通過註釋指定 mockgen
如上所述,如果有多個文件,並且分散在不同的位置,那麼我們要生成mock文件的時候,需要對每個文件執行多次mockgen命令假設套件名稱不相同)。這樣在真正操作起來的時候非常繁瑣,mockgen 還提供了一種通過註釋生成 mock 文件的方式,此時需要藉助 go 的 "go generate" 工具。

在接口文件的註釋裡面增加如下:
  1. //go:generate mockgen -destination mocks/mock_spider.go -package mocks mock_ex/spider Spider  
  2. type Spider interface {  
  3.     GetBody() string  
  4. }  
這樣,只要在 spider 目錄下執行:
# cd spider/
# go generate
# ls mocks/
mock_spider.go

就可以自動生成 mock 文件了。

gomock 的接口使用
在生成了mock實現代碼之後,我們就可以進行正常使用了。這裡假設結合testing進行使用(當然你也可考慮使用GoConvey)。我們就可以
在單元測試代碼裡面首先創建一個mock控制器:
  1. mockCtl := gomock.NewController(t)  

將 * testing.T 傳遞給 gomock 生成一個 "Controller" 對象,該對象控制了整個Mock的過程。在操作完後還需要進行回收,所以一般會在 New 後面 defer 一個 Finish
  1. defer mockCtl.Finish()  
然後就是調用 mock 生成代碼裡面為我們實現的接口對象:
  1. mockSpider := mocks.NewMockSpider(mockCtl)  
有了實現對象,我們就可以調用其斷言方法了: EXPECT()
  1. mockSpider.EXPECT().GetBody().Return("go1.8.3")  
這裡的每個 "." 調用都得到一個 "Call" 對象,該對像有如下方法:
  1. func (c *Call) After(preReq *Call) *Call  
  2. func (c *Call) AnyTimes() *Call  
  3. func (c *Call) Do(f interface{}) *Call  
  4. func (c *Call) MaxTimes(n int) *Call  
  5. func (c *Call) MinTimes(n int) *Call  
  6. func (c *Call) Return(rets ...interface{}) *Call  
  7. func (c *Call) SetArg(n int, value interface{}) *Call  
  8. func (c *Call) String() string  
  9. func (c *Call) Times(n int) *Call  
這裡 EXPECT() 得到實現的對象,然後調用實現對象的接口方法,接口方法返回第一個 "Call" 對象,然後對其進行條件約束。上面約束都可以在文檔中或者根據字面意思進行理解,這裡列舉幾個例子:

指定返回值
如我們的例子,調用Call的Return函數,可以指定接口的返回值:
  1. mockSpider.EXPECT().GetBody().Return("go1.8.3")  
這裡我們指定返回接口函數 GetBody() 返回"go1.8.3"。

指定執行次數
有時候我們需要指定函數執行多次,比如接受網絡請求的函數,計算其執行了多少次。
  1. mockSpider.EXPECT().Recv().Return(nil).Times(3)  
執行三次Recv函數,這裡還可以有另外幾種限制:
* AnyTimes() : 0到多次
* MaxTimes(n int) :最多執行n次,如果沒有設置
* MinTimes(n int) :最少執行n次,如果沒有設置

指定執行順序
有時候我們還要指定執行順序,比如要先執行Init操作,然後才能執行Recv操作。
  1. initCall := mockSpider.EXPECT().Init()  
  2. mockSpider.EXPECT().Recv().After(initCall)  
How To Run Testing
最後你可以如下執行測試:
# go test mock_ex/utils
ok mock_ex/utils (cached)

# go test utils/go_version_test.go
ok command-line-arguments 0.006s

Supplement
How to mock in your Go tests - Golang Tutorial (Youtube)
Medium - Go Test Your Code: An introduction to testing in Go


沒有留言:

張貼留言

[Git 常見問題] error: The following untracked working tree files would be overwritten by merge

  Source From  Here 方案1: // x -----删除忽略文件已经对 git 来说不识别的文件 // d -----删除未被添加到 git 的路径中的文件 // f -----强制运行 #   git clean -d -fx 方案2: 今天在服务器上  gi...