使用gomock對golang專案進行單元測試
熟悉golang的工程師應該都會利用golang自帶的go test工具對自己的程式碼進行單元測試,go test除了能夠自動的進行單元測試、輸出格式化結果之外,還可以輸出對應的覆蓋率統計,藉助覆蓋率統計資訊,我們可以看到單測中覆蓋到和沒有覆蓋到的程式碼行,從而對單測進行一定的優化。
gomock其實也是一個官方的、用於優化單測的工具。
gomock用在什麼地方
以下我們以一個例子說明什麼情況下需要用到gomock這個工具
src/fetcher/fetcher.go
package fetcher import ( "fmt" "net/http" ) type Fetcher interface { Get(string) (*http.Response, error) } func (httpFetcher *HttpFetcher) Get(query string) (*http.Response, error) { url := fmt.Sprintf("%s://%s:%d/%s", httpFetcher.Protocol, httpFetcher.Host, httpFetcher.Port, query) return http.Get(url) } type HttpFetcher struct { Host string Port int Protocol string } func ServiceCheck(query string, fetcher Fetcher) { resp, err := fetcher.Get(query) if err != nil { // todo: do something record fmt.Printf("%v", err) } if resp.StatusCode == 404 { // todo: do something record fmt.Println("status code is 404") return } else if resp.StatusCode == 400 { // todo: do something record fmt.Println("status code is 400") return } else if resp.StatusCode != 200 { // todo: do something record return } // todo: do something record for status code 200 return }
在fetcher.go函式中,定義了Fetcher這個介面型別,HttpFetcher實現了這個介面型別。而ServiceCheck這個函式,可以當作一個服務監控函式,它監控一個對應的url,當這個url返回的結果不為200時,它可以做一些記錄或者呼叫某一個回撥函式去報警。現在,如果我們需要對ServiceCheck這個函式去做單測,我們如何才能讓我們的單測能夠覆蓋到請求結果的狀態嗎不為200的這些分支呢?
甚至先不考慮這些非200分支,你能不能保證自己的單測能夠不受環境影響、獨立的執行?如果你在單測的時候提供一個指向測試環境服務的url,在你自己的測試環境下自然可以得到響應,但是離開這個測試環境,你的單測還能不能穩定的執行?
考慮到這些問題,我們需要一個mock工具,來解放在這個場景下對網路服務的依賴。如果我們可以mock出一個物件來替代HttpFetcher,並且重寫對應的Get函式,就可以按照我們的想法,輸出任意的*http.Response物件。
gomock介紹
gomock 是官方提供的 mock 框架,同時還提供了 mockgen 工具用來輔助生成測試程式碼。
使用如下命令即可安裝:
go get -u github.com/golang/mock/gomock
go get -u github.com/golang/mock/mockgen
安裝結束之後,$PATH下會新增一個mockgen的可執行檔案,通過以下命令:
mockgen -source=./fetcher.go -destination=./mock/mock_fetcher.go -package=mock
可以對Fetcher這個介面生成對應的mock物件,我們不需要關注生成mock_fetcher.go檔案的內容(看不懂,而且在使用中不需要關心)。隨後對fetcher.go寫單測:
src/fetcher/fetcher_test.go
package fetcher
import (
"net/http"
"testing"
fetcherMock"awesomeProject/fetcher/mock"
"github.com/golang/mock/gomock"
)
func TestHttpFetcher_Get_404(t *testing.T) {
query := "v4/resolve?dn=www.baidu.com&account_id=100195&ip=1.1.1.1&sign=9cc9723684867d" +
"5b5eb943431220790a&t=1866666666&cuid=xxxxxyyyyyzzzzz&type=dual_stack"
ctl := gomock.NewController(t)
defer ctl.Finish()
mock_resp := new(http.Response)
mock_resp.StatusCode = 404
mockFetcher := fetcherMock.NewMockFetcher(ctl)
mockFetcher.EXPECT().Get(query).Return(mock_resp, nil)
ServiceCheck(url, mockFetcher)
}
對應的目錄樹結構為:
├── fetcher.go
├── fetcher_test.go
└── mock
└── mock_fetcher.go
- gomock.NewController:返回
gomock.Controller
,它代表 mock 生態系統中的頂級控制元件。定義了 mock 物件的範圍、生命週期和期待值。另外它在多個 goroutine 中是安全的 - mock.NewMockFetcher:建立一個新的 mock 例項
- mockFetcher.EXPECT().Get(url).Return(mock_resp, nil):這裡有三個步驟,
EXPECT()
返回一個允許呼叫者設定期望和返回值的物件。Get(query)
是設定入參並呼叫 mock 例項中的方法。Return(mock_resp, nil)
是設定先前呼叫的方法出參。簡單來說,就是設定入參並呼叫,最後設定返回值
在fetcher/目錄下執行go test,有以下輸出:
status code is 404 PASS ok awesomeProject/fetcher 0.586s
可見mock成功了。