GoMock架構使用指南

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

序言

要寫出好的測試代碼,必須精通相關的測試架構。對於Golang的程式員來說,至少需要掌握下面三個測試架構:

  • GoConvey
  • GoStub
  • GoMock

讀者通過前面三篇文章的學習可以對架構GoConvey和GoStub優雅的組合使用了,本文將接著介紹第三個架構GoMock的使用方法,目的是使得讀者掌握架構GoConvey + GoStub + GoMock組合使用的正確姿勢,從而提高測試代碼的品質。

GoMock是由Golang官方開發維護的測試架構,實現了較為完整的基於interface的Mock功能,能夠與Golang內建的testing包良好整合,也能用於其它的測試環境中。GoMock測試架構套件含了GoMock包和mockgen工具兩部分,其中GoMock包完成對樁對象生命週期的管理,mockgen工具用來產生interface對應的Mock類源檔案。

安裝

在命令列運行命令:

go get github.com/golang/mock/gomock

運行完後你會發現,在$GOPATH/src目錄下有了github.com/golang/mock子目錄,且在該子目錄下有GoMock包和mockgen工具。

繼續運行命令:

cd $GOPATH/src/github.com/golang/mock/mockgengo build

則在目前的目錄下產生了一個可執行程式mockgen。

將mockgen程式移動到$GOPATH/bin目錄下:

mv mockgen $GOPATH/bin

這時在命令列運行mockgen,如果列出了mockgen的使用方法和例子,則說明mockgen已經安裝成功,否則會顯示:

-bash: mockgen: command not found

一般是由於沒有在環境變數PATH中配置$GOPATH/bin導致。

文檔

GoMock架構安裝完成後,可以使用go doc命令來擷取文檔:

go doc github.com/golang/mock/gomock

另外,有一個線上的參考文檔,即package gomock。

使用方法

定義一個介面

我們先定義一個打算mock的介面Repository:

package dbtype Repository interface {    Create(key string, value []byte) error    Retrieve(key string) ([]byte, error)    Update(key string, value []byte) error    Delete(key string) error}

Repository是領域驅動設計中戰術設計的一個元素,用來儲存領域對象,一般將對象持久化在資料庫中,比如Aerospike,Redis或Etcd等。對於領域層來說,只知道對象在Repository中維護,並不care對象到底在哪持久化,這是基礎設施層的職責。微服務在啟動時,根據部署參數執行個體化Repository介面,比如AerospikeRepository,RedisRepository或EtcdRepository。

假設有一個領域對象Movie要進行持久化,則先要通過json.Marshal進行序列化,然後再調用Repository的Create方法來儲存。當要根據key(實體Id)尋找領域對象時,則先通過Repository的Retrieve方法獲得領域對象的位元組切片,然後通過json.Unmarshal進行還原序列化的到領域對象。當領域對象的資料有變化時,則先要通過json.Marshal進行序列化,然後再調用Repository的Update方法來更新。當領域對象生命週期結束而要消亡時,則直接調用Repository的Delete方法進行刪除。

產生mock類檔案

這下該mockgen工具登場了。mockgen有兩種操作模式:源檔案和反射。

源檔案模式通過一個包含interface定義的檔案產生mock類檔案,它通過 -source 標識生效,-imports 和 -aux_files 標識在這種模式下也是有用的。
舉例:

mockgen -source=foo.go [other options]

反射模式通過構建一個程式用反射理解介面產生一個mock類檔案,它通過兩個非標誌參數生效:匯入路徑和用逗號分隔的符號列表(多個interface)。
舉例:

mockgen database/sql/driver Conn,Driver

注意:第一個參數是基於GOPATH的相對路徑,第二個參數可以為多個interface,並且interface之間只能用逗號分隔,不能有空格。

有一個包含打算Mock的interface的源檔案,就可用mockgen命令產生一個mock類的源檔案。mockgen支援的選項如下:

  • -source: 一個檔案包含打算mock的介面列表
  • -destination: 存放mock類代碼的檔案。如果你沒有設定這個選項,代碼將被列印到標準輸出
  • -package: 用於指定mock類源檔案的包名。如果你沒有設定這個選項,則包名由mock_和輸入檔案的包名級聯而成
  • -aux_files: 參看附加的檔案清單是為瞭解析類似嵌套的定義在不同檔案中的interface。指定元素列表以逗號分隔,元素形式為foo=bar/baz.go,其中bar/baz.go是源檔案,foo是-source選項指定的源檔案用到的包名

在簡單的情境下,你將只需使用-source選項。在複雜的情況下,比如一個檔案定義了多個interface而你只想對部分interface進行mock,或者interface存在嵌套,這時你需要用反射模式。由於 -destination 選項輸入太長,筆者一般不使用該標識符,而使用重新導向符號 >,並且mock類代碼的輸出檔案的路徑必須是絕對路徑。

現在我們運行mockgen命令通過反射模式產生Repository的Mock類源檔案:

mockgen infra/db Repository > $GOPATH/src/test/mock/mock_repository.go

注意:

  1. 輸出目錄test/mock必須提前建好,否則mockgen會運行失敗
  2. 如果你的工程中的第三方庫統一放在vendor目錄下,則需要拷貝一份gomock的代碼到$GOPATH/src下,gomock的代碼即github.com/golang/mock/gomock,這是因為mockgen命令運行時要在這個路徑訪問gomock

可以在test/mock目錄下看到mock_repository.go檔案已經產生,該檔案的程式碼片段如下:

// Automatically generated by MockGen. DO NOT EDIT!// Source: infra/db (interfaces: Repository)package mock_dbimport (    gomock "github.com/golang/mock/gomock")// MockRepository is a mock of Repository interfacetype MockRepository struct {    ctrl     *gomock.Controller    recorder *MockRepositoryMockRecorder}// MockRepositoryMockRecorder is the mock recorder for MockRepositorytype MockRepositoryMockRecorder struct {    mock *MockRepository}// NewMockRepository creates a new mock instancefunc NewMockRepository(ctrl *gomock.Controller) *MockRepository {    mock := &MockRepository{ctrl: ctrl}    mock.recorder = &MockRepositoryMockRecorder{mock}    return mock}// EXPECT returns an object that allows the caller to indicate expected usefunc (_m *MockRepository) EXPECT() *MockRepositoryMockRecorder {    return _m.recorder}// Create mocks base methodfunc (_m *MockRepository) Create(_param0 string, _param1 []byte) error {    ret := _m.ctrl.Call(_m, "Create", _param0, _param1)    ret0, _ := ret[0].(error)    return ret0}// Create indicates an expected call of Createfunc (_mr *MockRepositoryMockRecorder) Create(arg0, arg1 interface{}) *gomock.Call {    return _mr.mock.ctrl.RecordCall(_mr.mock, "Create", arg0, arg1)}...

使用mock對象進行打樁測試

mock類源檔案產生後,就可以寫測試案例了。

匯入mock相關的包

mock相關的包包括testing,gmock和mock_db,import包路徑:

import (    "testing"    . "github.com/golang/mock/gomock"    "test/mock"    ...)

mock控制器

mock控制器通過NewController介面產生,是mock生態系統的頂層控制,它定義了mock對象的範圍和生命週期,以及它們的期望。多個協程同時調用控制器的方法是安全的。
當用例結束後,控制器會檢查所有剩餘期望的調用是否滿足條件。

控制器的代碼如下所示:

ctrl := gomock.NewController(t)defer ctrl.Finish()

mock對象建立時需要注入控制器,如果有多個mock對象則注入同一個控制器,如下所示:

ctrl := gomock.NewController(t)defer ctrl.Finish()mockRepo := mock_db.NewMockRepository(ctrl)mockHttp := mock_api.NewHttpMethod(ctrl)

mock對象的行為注入

對於mock對象的行為注入,控制器是通過map來維護的,一個方法對應map的一項。因為一個方法在一個用例中可能調用多次,所以map的實值型別是數組切片。當mock對象進行行為注入時,控制器會將行為Add。當該方法被調用時,控制器會將該行為Remove。

假設有這樣一個情境:先Retrieve領域對象失敗,然後Create領域對象成功,再次Retrieve領域對象就能成功。這個情境對應的mock對象的行為注入代碼如下所示:

mockRepo.EXPECT().Retrieve(Any()).Return(nil, ErrAny)mockRepo.EXPECT().Create(Any(), Any()).Return(nil)mockRepo.EXPECT().Retrieve(Any()).Return(objBytes, nil)

objBytes是領域對象的序列化結果,比如:

obj := Movie{...}objBytes, err := json.Marshal(obj)...

當批量Create對象時,可以使用Times關鍵字:

mockRepo.EXPECT().Create(Any(), Any()).Return(nil).Times(5)

當批量Retrieve對象時,需要注入多次mock行為:

mockRepo.EXPECT().Retrieve(Any()).Return(objBytes1, nil)mockRepo.EXPECT().Retrieve(Any()).Return(objBytes2, nil)mockRepo.EXPECT().Retrieve(Any()).Return(objBytes3, nil)mockRepo.EXPECT().Retrieve(Any()).Return(objBytes4, nil)mockRepo.EXPECT().Retrieve(Any()).Return(objBytes5, nil)

行為調用的保序

預設情況下,行為調用順序可以和mock對象行為注入順序不一致,即不保序。如果要保序,有兩種方法:

  1. 通過After關鍵字來實現保序
  2. 通過InOrder關鍵字來實現保序

通過After關鍵字實現的保序範例程式碼:

firstCall := mockObj.EXPECT().SomeMethod(1, "first")secondCall := mockObj.EXPECT().SomeMethod(2, "second").After(firstCall)mockObj.EXPECT().SomeMethod(3, "third").After(secondCall)

通過InOrder關鍵字實現的保序範例程式碼:

InOrder(    mockObj.EXPECT().SomeMethod(1, "first"),    mockObj.EXPECT().SomeMethod(2, "second"),    mockObj.EXPECT().SomeMethod(3, "third"),)

顯然,InOrder關鍵字實現的保序更簡單自然,所以推薦這種方式。其實,關鍵字InOrder是After的文法糖,不信你看:

// InOrder declares that the given calls should occur in order.func InOrder(calls ...*Call) {    for i := 1; i < len(calls); i++ {        calls[i].After(calls[i-1])    }}

當mock對象行為的注入保序後,如果行為調用的順序和其不一致,則測試失敗。這就是說,對於上面的例子,如果在測試案例執行過程中,SomeMethod方法的調用不是按照SomeMethod(1, "first") -> SomeMethod(2, "second") -> SomeMethod(3, "third") 的順序進行,則測試失敗。

mock對象的注入

mock對象的行為都注入到控制器以後,我們接著要將mock對象注入給interface,使得mock對象在測試中生效。
在使用GoStub架構之前,很多人都使用土方法,比如Set。這種方法有一個缺陷:當測試案例執行完成後,並沒有復原interface到真實對象,有可能會影響其它測試案例的執行。所以,筆者強烈建議大家使用GoStub架構完成mock對象的注入。

stubs := StubFunc(&redisrepo.GetInstance, mockDb)defer stubs.Reset()

測試Demo

編寫測試案例有一些基本原則,我們一起回顧一下:

  1. 每個測試案例只關注一個問題,不要寫大而全的測試案例
  2. 測試案例是黑盒的
  3. 測試案例之間彼此獨立,每個用例要保證自己的前置和後置完備
  4. 測試案例要對產品代碼非入侵
  5. ...

根據基本原則,我們不要在一個測試函數的多個測試案例之間共用mock控制器,於是就有了下面的Demo:

func TestObjDemo(t *testing.T) {    Convey("test obj demo", t, func() {        Convey("create obj", func() {            ctrl := NewController(t)            defer ctrl.Finish()            mockRepo := mock_db.NewMockRepository(ctrl)            mockRepo.EXPECT().Retrieve(Any()).Return(nil, ErrAny)            mockRepo.EXPECT().Create(Any(), Any()).Return(nil)            mockRepo.EXPECT().Retrieve(Any()).Return(objBytes, nil)            stubs := StubFunc(&redisrepo.GetInstance, mockRepo)            defer stubs.Reset()            ...        })        Convey("bulk create objs", func() {            ctrl := NewController(t)            defer ctrl.Finish()            mockRepo := mock_db.NewMockRepository(ctrl)            mockRepo.EXPECT().Create(Any(), Any()).Return(nil).Times(5)            stubs := StubFunc(&redisrepo.GetInstance, mockRepo)            defer stubs.Reset()            ...        })        Convey("bulk retrieve objs", func() {            ctrl := NewController(t)            defer ctrl.Finish()            mockRepo := mock_db.NewMockRepository(ctrl)            objBytes1 := ...            objBytes2 := ...            objBytes3 := ...            objBytes4 := ...            objBytes5 := ...            mockRepo.EXPECT().Retrieve(Any()).Return(objBytes1, nil)            mockRepo.EXPECT().Retrieve(Any()).Return(objBytes2, nil)            mockRepo.EXPECT().Retrieve(Any()).Return(objBytes3, nil)            mockRepo.EXPECT().Retrieve(Any()).Return(objBytes4, nil)            mockRepo.EXPECT().Retrieve(Any()).Return(objBytes5, nil)            stubs := StubFunc(&redisrepo.GetInstance, mockRepo)            defer stubs.Reset()            ...        })    })    ...}

小結

本文詳細闡述了GoMock架構的使用方法,不但結合例子給出了標準用法,而且列出了很多要點,最後通過一個簡單的測試Demo說明了GoConvey + GoStub + GoMock組合使用的正確姿勢。希望讀者舉一反三,同時將前面三篇的核心內容融入進來,寫出高品質的測試代碼,最終提升產品品質。

至此,我們已經知道:

  1. 全域變數可通過GoStub架構打樁
  2. 過程可通過GoStub架構打樁
  3. 函數可通過GoStub架構打樁
  4. interface可通過GoMock架構打樁

於是問題來了,方法通過神馬打樁?
或許有好的解決方案,但由於筆者才疏學淺,目前還沒有想到成熟的解決方案,只能盡量弱化方法調用對單元測試的影響。如果方法的調用層次很深,而且中介層都是方法,則可能導致打樁的複雜度比較高,這時需要在適當的層次引入interface,以便單元測試時通過對interface打樁將調用鏈從中間截斷,從而有效降低打樁的複雜度。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.