1. 程式人生 > 實用技巧 >使用gomock對golang專案進行單元測試

使用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

  1. gomock.NewController:返回 gomock.Controller,它代表 mock 生態系統中的頂級控制元件。定義了 mock 物件的範圍、生命週期和期待值。另外它在多個 goroutine 中是安全的
  2. mock.NewMockFetcher:建立一個新的 mock 例項
  3. 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成功了。