2016年我寫過一篇關於Go語言單元測試的文章,簡單介紹了 testing 庫的使用方法。後來發現 testify/require 和 testify/assert 能夠大大簡化單元測試的寫法,徹底能夠替代 t.Fatalf
和 t.Errorf
,並且代碼實現更爲簡短、優雅。javascript
再後來,發現了 mockery 庫,它能夠爲 Go interface 生成一個 mocks struct。經過 mocks struct,在單元測試中咱們能夠模擬全部 normal cases 和 corner cases,完全消除細節實現上的bug。mocks 在測試無狀態函數 (對應 FP 中的 pure function) 中意義不大,其應用場景主要在於處理不可控的第三方服務、數據庫、磁盤讀寫等。若是這些服務的調用細節已經被封裝到 interface 內部,調用方只看到了 interface 定義的一組方法,那麼在測試中 mocks 就能控制第三方服務返回任意指望的結果,進而實現對調用方邏輯的全方位測試。html
關於 interface 的諸多用法,我會單獨拎出來一篇文章來說。本文中,我會經過兩個例子展現 testify/require
和 mockery
的用法,分別是:java
testify/require
簡化 table driven testmockery
和 testify/mock
爲 lazy cache 寫單元測試# download require, assert, mock go get -u -v github.com/stretchr/testify # install mockery into GoBin go get -u -v github.com/vektra/mockery/.../
首先,咱們經過一個簡單的例子看下 require 的用法。咱們針對函數 Sqrt
進行測試,其實現爲:git
// Sqrt calculate the square root of a non-negative float64 // number with max error of 10^-9. For simplicity, we don't // discard the part with is smaller than 10^-9. func Sqrt(x float64) float64 { if x < 0 { panic("cannot be negative") } if x == 0 { return 0 } a := x / 2 b := (a + 2) / 2 erro := a - b for erro >= 0.000000001 || erro <= -0.000000001 { a = b b = (b + x/b) / 2 erro = a - b } return b }
這裏咱們使用了一個常規的方法實現 Sqrt
,該實現的最大精確度是到小數點後9位(爲了方便演示,這裏沒有對超出9位的部分進行刪除)。咱們首先測試 x < 0
致使 panic 的狀況,看 require
如何使用,測試代碼以下:github
func TestSqrt_Panic(t *testing.T) { defer func() { r := recover() require.Equal(t, "cannot be negative", r) }() _ = Sqrt(-1) }
在上面的函數中,咱們只使用 require.Equal
一行代碼就實現了運行結果校驗。若是使用 testing
來實現的話,變成了三行,而且須要手寫一串描述:golang
func TestSqrt_Panic(t *testing.T) { defer func() { r := recover() if r.(string) != "cannot be negative" { t.Fatalf("expect to panic with message \"cannot be negative\", but got \"%s\"\n", r) } }() _ = Sqrt(-1) }
使用 require
以後,不只使測試代碼更易於編寫,並且可以在測試運行失敗時,格式化運行結果,方便定位和修改bug。這裏你不妨把 -1
改爲一個正數,運行 go test
,查看運行結果。數據庫
上面咱們可以看到 require
庫帶來的編碼和調試效率的上升。在 table driven test 中,咱們會有更深入的體會。api
咱們仍然以 Sqrt
爲例,來看下如何在 table driven test 中使用 require
。這裏咱們測試的傳入常規參數的狀況,代碼實現以下:微信
func TestSqrt(t *testing.T) { testcases := []struct { desc string input float64 expect float64 }{ { desc: "zero", input: 0, expect: 0, }, { desc: "one", input: 1, expect: 1, }, { desc: "a very small rational number", input: 0.00000000000000000000000001, expect: 0.0, }, { desc: "rational number result: 2.56", input: 2.56, expect: 1.6, }, { desc: "irrational number result: 2", input: 2, expect: 1.414213562, }, } for _, ts := range testcases { got := Sqrt(ts.input) erro := got - ts.expect require.True(t, erro < 0.000000001 && erro > -0.000000001, ts.desc) } }
在上面這個例子,有三點值得注意:函數
匿名struct
容許咱們填充任意類型的字段,很是方便於構建測試數據集;匿名struct
都包含一個 desc string
字段,用於描述該測試要處理的情況。在測試運行失敗時,很是有助於定位失敗位置;require
而不是 assert
,由於使用 require
時,測試失敗之後,全部測試都會中止執行。關於 require
,除了本文中提到的 require.True
, require.Equal
,還有一個比較實用的方法是 require.EqualValues
,它的應用場景在於處理 Go 的強類型問題,咱們不妨看一段代碼:
func Test_Require_EqualValues(t *testing.T) { // tests will pass require.EqualValues(t, 12, 12.0, "compare int32 and float64") require.EqualValues(t, 12, int64(12), "compare int32 and int64") // tests will fail require.Equal(t, 12, 12.0, "compare int32 and float64") require.Equal(t, 12, int64(12), "compare int32 and int64") }
更多 require
的方法參考 require's godoc。
mockery 與 Go 指令(directive) 結合使用,咱們能夠爲 interface 快速建立對應的 mock struct。即使沒有具體實現,也能夠被其餘包調用。咱們經過 LazyCache 的例子來看它的使用方法。
假設有一個第三方服務,咱們把它封裝在 thirdpartyapi
包裏,並加入 go directive,代碼以下:
package thirdpartyapi //go:generate mockery -name=Client // Client defines operations a third party service has type Client interface { Get(key string) (data interface{}, err error) }
咱們在 thirdpartyapi 目錄下執行 go generate
,在 mocks 目錄下生成對應的 mock struct。目錄結構以下:
~ $ tree thirdpartyapi/ thirdpartyapi/ ├── client.go └── mocks └── Client.go 1 directory, 2 files
在執行 go generate
時,指令 //go:generate mockery -name=Client
被觸發。它本質上是 mockery -name=Client
的快捷方式,優點是 go generate 能夠批量執行多個目錄下的多個指令(須要多加一個參數,具體能夠參考文檔)。
此時,咱們只有 interface,並無具體的實現,可是不妨礙在 LazyCache
中調用它,也不妨礙在測試中調用 thirdpartyapi
的 mocks client。爲了方便理解,這裏把 LazyCache
的實現也貼出來 (忽略 import):
//go:generate mockery -name=LazyCache // LazyCache defines the methods for the cache type LazyCache interface { Get(key string) (data interface{}, err error) } // NewLazyCache instantiates a default lazy cache implementation func NewLazyCache(client thirdpartyapi.Client, timeout time.Duration) LazyCache { return &lazyCacheImpl{ cacheStore: make(map[string]cacheValueType), thirdPartyClient: client, timeout: timeout, } } type cacheValueType struct { data interface{} lastUpdated time.Time } type lazyCacheImpl struct { sync.RWMutex cacheStore map[string]cacheValueType thirdPartyClient thirdpartyapi.Client timeout time.Duration // cache would expire after timeout } // Get implements LazyCache interface func (c *lazyCacheImpl) Get(key string) (data interface{}, err error) { c.RLock() val := c.cacheStore[key] c.RUnlock() timeNow := time.Now() if timeNow.After(val.lastUpdated.Add(c.timeout)) { // fetch data from third party service and update cache latest, err := c.thirdPartyClient.Get(key) if err != nil { return nil, err } val = cacheValueType{latest, timeNow} c.Lock() c.cacheStore[key] = val c.Unlock() } return val.data, nil }
爲了簡單,咱們暫時不考慮 cache miss 或 timeout 與cache被更新的時間間隙,大量請求直接打到 thirdpartyapi
可能致使的後果。
介紹測試以前,咱們首先了解一下 "控制變量法",在天然科學中,它被普遍用於各種實驗中。在智庫百科,它被定義爲 指把多因素的問題變成多個單因素的問題,而只改變其中的某一個因素,從而研究這個因素對事物影響,分別加以研究,最後再綜合解決的方法。該方法一樣適用於計算機科學,尤爲是測試不一樣場景下程序是否能如指望般運行。咱們將這種方法應用於本例中 Get
方法的測試。
在 Get
方法中,可變因素有 cacheStore
、thirdPartyClient
和 timeout
。在測試中,cacheStore
和 timeout
是徹底可控的,thirdPartyClient
的行爲須要經過 mocks 自定義指望行爲以覆蓋默認實現。事實上,mocks 的功能要強大的多,下面咱們用代碼來看。
這裏,我只拿出 Cache Miss Update Failure 一個case 來分析,覆蓋全部 case 的代碼查看 github repo。
func TestGet_CacheMiss_Update_Failure(t *testing.T) { testKey := "test_key" errTest := errors.New("test error") mockThirdParty := &mocks.Client{} mockThirdParty.On("Get", testKey).Return(nil, errTest).Once() mockCache := &lazyCacheImpl{ memStore: map[string]cacheValueType{}, thirdPartyClient: mockThirdParty, timeout: testTimeout, } // test cache miss, fails to fetch from data source _, gotErr := mockCache.Get(testKey) require.Equal(t, errTest, gotErr) mock.AssertExpectationsForObjects(t, mockThirdParty) }
這裏,咱們只討論 mockThirdParty
,主要有三點:
mockThirdParty.On("Get", testKey).Return(nil, errTest).Once()
用於定義該對象 Get
方法的行爲:Get
方法接受 testKey
做爲參數,當且僅當被調用一次時,會返回 errTest
。若是一樣的參數,被調用第二次,就會報錯;_, gotErr := mockCache.Get(testKey)
觸發一次上一步中定義的行爲;mock.AssertExpectationsForObjects
函數會對傳入對象進行檢查,保證預約義的指望行爲徹底被精確地觸發;在 table driven test 中,咱們能夠經過 mockThirdParty.On
方法定義 Get
針對不一樣參數返回不一樣的結果。
在上面的測試中 .Once()
等價於 .Times(1)
。若是去掉 .Once()
,意味着 mockThirdParty.Get
方法能夠被調用任意次。
更多 mockery 的使用方法參考 github
在本文中,咱們結合實例講解了 testify
和 mockery
兩個庫在單元測試中的做用。最後分享一個圖,但願你們能重視單元測試。
掃碼關注微信公衆號「深刻Go語言」