使用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成功了。這樣的mock方式稱為"打樁",有明確的參數和對應的返回值。除此之外還有其它的打樁方式,有需要的可以自己去查詢。

編寫可測試的代碼

如果仔細看以上的用例,可以發現gomock只能mock一個滿足interface的類型,它對一個具體的類型是無能為力的。比如,如果我們使用這個函數定義:

func ServiceCheck(query string, fetcher HttpFetcher)

gomock是沒辦法mock出一個HttpFetcher實例的。而代碼的可測試性和代碼的可讀性、高效性同等重要,因此,將依賴抽象為接口類型,不僅可以降低耦合度,還有利於寫出方便測試的代碼。

如果再仔細看這個函數,會發現它同樣使用了依賴注入的方式降低耦合性。如果Fetcher的實例放在ServiceCheck函數內部去初始化,那么不僅是函數可測試性大打折扣,某個實現了Fetcher的類型也與ServiceCheck函數形成了緊耦合。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM