序言
要寫出好的測試代碼浮还,必須精通相關(guān)的測試框架。對于Golang的程序員來說翎承,至少需要掌握下面四個測試框架:
GoConvey
GoStub
GoMock
Monkey
讀者通過前面三篇文章的學(xué)習(xí)可以對框架GoConvey和GoStub優(yōu)雅的組合使用了顾画,本文將接著介紹第三個框架GoMock的使用方法,目的是使得讀者掌握框架GoConvey + GoStub + GoMock組合使用的正確姿勢蒲每,從而提高測試代碼的質(zhì)量。
GoMock是由Golang官方開發(fā)維護(hù)的測試框架喻括,實現(xiàn)了較為完整的基于interface的Mock功能邀杏,能夠與Golang內(nèi)置的testing包良好集成,也能用于其它的測試環(huán)境中唬血。GoMock測試框架包含了GoMock包和mockgen工具兩部分望蜡,其中GoMock包完成對樁對象生命周期的管理,mockgen工具用來生成interface對應(yīng)的Mock類源文件拷恨。
安裝
在命令行運行命令:
goget github.com/golang/mock/gomock
運行完后你會發(fā)現(xiàn)脖律,在$GOPATH/src目錄下有了github.com/golang/mock子目錄,且在該子目錄下有GoMock包和mockgen工具腕侄。
繼續(xù)運行命令:
cd $GOPATH/src/github.com/golang/mock/mockgengobuild
則在當(dāng)前目錄下生成了一個可執(zhí)行程序mockgen小泉。
將mockgen程序移動到$GOPATH/bin目錄下:
mv mockgen $GOPATH/bin
這時在命令行運行mockgen,如果列出了mockgen的使用方法和例子冕杠,則說明mockgen已經(jīng)安裝成功微姊,否則會顯示:
-bash: mockgen: command not found
一般是由于沒有在環(huán)境變量PATH中配置$GOPATH/bin導(dǎo)致。
文檔
GoMock框架安裝完成后拌汇,可以使用go doc命令來獲取文檔:
godoc github.com/golang/mock/gomock
另外柒桑,有一個在線的參考文檔,即package gomock噪舀。
使用方法
定義一個接口
我們先定義一個打算mock的接口Repository:
packagedbtypeRepositoryinterface{? ? Create(keystring, value []byte) error? ? Retrieve(keystring) ([]byte, error)? ? Update(keystring, value []byte) error? ? Delete(keystring) error}
Repository是領(lǐng)域驅(qū)動設(shè)計中戰(zhàn)術(shù)設(shè)計的一個元素魁淳,用來存儲領(lǐng)域?qū)ο笃话銓ο蟪志没跀?shù)據(jù)庫中,比如Aerospike界逛,Redis或Etcd等昆稿。對于領(lǐng)域?qū)觼碚f,只知道對象在Repository中維護(hù)息拜,并不care對象到底在哪持久化溉潭,這是基礎(chǔ)設(shè)施層的職責(zé)。微服務(wù)在啟動時少欺,根據(jù)部署參數(shù)實例化Repository接口喳瓣,比如AerospikeRepository,RedisRepository或EtcdRepository赞别。
假設(shè)有一個領(lǐng)域?qū)ο驧ovie要進(jìn)行持久化畏陕,則先要通過json.Marshal進(jìn)行序列化,然后再調(diào)用Repository的Create方法來存儲仿滔。當(dāng)要根據(jù)key(實體Id)查找領(lǐng)域?qū)ο髸r惠毁,則先通過Repository的Retrieve方法獲得領(lǐng)域?qū)ο蟮淖止?jié)切片,然后通過json.Unmarshal進(jìn)行反序列化的到領(lǐng)域?qū)ο笃橐场.?dāng)領(lǐng)域?qū)ο蟮臄?shù)據(jù)有變化時鞠绰,則先要通過json.Marshal進(jìn)行序列化,然后再調(diào)用Repository的Update方法來更新飒焦。當(dāng)領(lǐng)域?qū)ο笊芷诮Y(jié)束而要消亡時蜈膨,則直接調(diào)用Repository的Delete方法進(jìn)行刪除。
生成mock類文件
這下該mockgen工具登場了牺荠。mockgen有兩種操作模式:源文件和反射丈挟。
源文件模式通過一個包含interface定義的文件生成mock類文件,它通過 -source 標(biāo)識生效志电,-imports 和 -aux_files 標(biāo)識在這種模式下也是有用的。
舉例:
mockgen -source=foo.go[other options]
反射模式通過構(gòu)建一個程序用反射理解接口生成一個mock類文件蛔趴,它通過兩個非標(biāo)志參數(shù)生效:導(dǎo)入路徑和用逗號分隔的符號列表(多個interface)挑辆。
舉例:
mockgen database/sql/driver Conn,Driver
注意:第一個參數(shù)是基于GOPATH的相對路徑,第二個參數(shù)可以為多個interface孝情,并且interface之間只能用逗號分隔鱼蝉,不能有空格。
有一個包含打算Mock的interface的源文件箫荡,就可用mockgen命令生成一個mock類的源文件魁亦。mockgen支持的選項如下:
-source: 一個文件包含打算mock的接口列表
-destination: 存放mock類代碼的文件。如果你沒有設(shè)置這個選項羔挡,代碼將被打印到標(biāo)準(zhǔn)輸出
-package: 用于指定mock類源文件的包名洁奈。如果你沒有設(shè)置這個選項间唉,則包名由mock_和輸入文件的包名級聯(lián)而成
-aux_files: 參看附加的文件列表是為了解析類似嵌套的定義在不同文件中的interface。指定元素列表以逗號分隔利术,元素形式為foo=bar/baz.go呈野,其中bar/baz.go是源文件,foo是-source選項指定的源文件用到的包名
在簡單的場景下印叁,你將只需使用-source選項被冒。在復(fù)雜的情況下,比如一個文件定義了多個interface而你只想對部分interface進(jìn)行mock轮蜕,或者interface存在嵌套昨悼,這時你需要用反射模式。由于 -destination 選項輸入太長跃洛,筆者一般不使用該標(biāo)識符率触,而使用重定向符號 >,并且mock類代碼的輸出文件的路徑必須是絕對路徑税课。
現(xiàn)在我們運行mockgen命令通過反射模式生成Repository的Mock類源文件:
mockgen infra/db Repository > $GOPATH/src/test/mock/db/mock_repository.go
注意:
輸出目錄test/mock/db必須提前建好闲延,否則mockgen會運行失敗
如果你的工程中的第三方庫統(tǒng)一放在vendor目錄下,則需要拷貝一份gomock的代碼到$GOPATH/src下韩玩,gomock的代碼即github.com/golang/mock/gomock垒玲,這是因為mockgen命令運行時要在這個路徑訪問gomock
可以在test/mock/db目錄下看到mock_repository.go文件已經(jīng)生成,該文件的代碼片段如下:
// Automatically generated by MockGen. DO NOT EDIT!// Source: infra/db (interfaces: Repository)packagemock_dbimport(? ? gomock"github.com/golang/mock/gomock")// MockRepository is a mock of Repository interfacetypeMockRepositorystruct{? ? ctrl? ? *gomock.Controller? ? recorder *MockRepositoryMockRecorder}// MockRepositoryMockRecorder is the mock recorder for MockRepositorytypeMockRepositoryMockRecorderstruct{? ? mock *MockRepository}// NewMockRepository creates a new mock instancefuncNewMockRepository(ctrl *gomock.Controller)*MockRepository{? ? mock := &MockRepository{ctrl: ctrl}? ? mock.recorder = &MockRepositoryMockRecorder{mock}returnmock}// 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(_param0string, _param1 []byte)error{? ? ret := _m.ctrl.Call(_m,"Create", _param0, _param1)? ? ret0, _ := ret[0].(error)returnret0}// Create indicates an expected call of Createfunc(_mr *MockRepositoryMockRecorder)Create(arg0, arg1interface{})*gomock.Call{return_mr.mock.ctrl.RecordCall(_mr.mock,"Create", arg0, arg1)}...
使用mock對象進(jìn)行打樁測試
mock類源文件生成后找颓,就可以寫測試用例了合愈。
導(dǎo)入mock相關(guān)的包
mock相關(guān)的包包括testing,gmock和mock_db击狮,import包路徑:
import("testing"."github.com/golang/mock/gomock""test/mock/db"...)
mock控制器
mock控制器通過NewController接口生成佛析,是mock生態(tài)系統(tǒng)的頂層控制,它定義了mock對象的作用域和生命周期彪蓬,以及它們的期望寸莫。多個協(xié)程同時調(diào)用控制器的方法是安全的。
當(dāng)用例結(jié)束后档冬,控制器會檢查所有剩余期望的調(diào)用是否滿足條件膘茎。
控制器的代碼如下所示:
ctrl := NewController(t)deferctrl.Finish()
mock對象創(chuàng)建時需要注入控制器,如果有多個mock對象則注入同一個控制器酷誓,如下所示:
ctrl := NewController(t)deferctrl.Finish()mockRepo := mock_db.NewMockRepository(ctrl)mockHttp := mock_api.NewHttpMethod(ctrl)
mock對象的行為注入
對于mock對象的行為注入披坏,控制器是通過map來維護(hù)的,一個方法對應(yīng)map的一項盐数。因為一個方法在一個用例中可能調(diào)用多次棒拂,所以map的值類型是數(shù)組切片。當(dāng)mock對象進(jìn)行行為注入時玫氢,控制器會將行為Add帚屉。當(dāng)該方法被調(diào)用時谜诫,控制器會將該行為Remove。
假設(shè)有這樣一個場景:先Retrieve領(lǐng)域?qū)ο笫′汤缓驝reate領(lǐng)域?qū)ο蟪晒Σ滦澹俅蜶etrieve領(lǐng)域?qū)ο缶湍艹晒Α_@個場景對應(yīng)的mock對象的行為注入代碼如下所示:
mockRepo.EXPECT().Retrieve(Any()).Return(nil, ErrAny)mockRepo.EXPECT().Create(Any(), Any()).Return(nil)mockRepo.EXPECT().Retrieve(Any()).Return(objBytes,nil)
objBytes是領(lǐng)域?qū)ο蟮男蛄谢Y(jié)果敬特,比如:
obj := Movie{...}
objBytes, err := json.Marshal(obj)
...
當(dāng)批量Create對象時掰邢,可以使用Times關(guān)鍵字:
mockRepo.EXPECT().Create(Any(), Any()).Return(nil).Times(5)
當(dāng)批量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)
行為調(diào)用的保序
默認(rèn)情況下伟阔,行為調(diào)用順序可以和mock對象行為注入順序不一致辣之,即不保序。如果要保序皱炉,有兩種方法:
通過After關(guān)鍵字來實現(xiàn)保序
通過InOrder關(guān)鍵字來實現(xiàn)保序
通過After關(guān)鍵字實現(xiàn)的保序示例代碼:
retrieveCall := mockRepo.EXPECT().Retrieve(Any()).Return(nil, ErrAny)createCall := mockRepo.EXPECT().Create(Any(), Any()).Return(nil).After(retrieveCall)mockRepo.EXPECT().Retrieve(Any()).Return(objBytes,nil).After(createCall)
通過InOrder關(guān)鍵字實現(xiàn)的保序示例代碼:
InOrder(? ? mockRepo.EXPECT().Retrieve(Any()).Return(nil, ErrAny)? ? mockRepo.EXPECT().Create(Any(), Any()).Return(nil)? ? mockRepo.EXPECT().Retrieve(Any()).Return(objBytes,nil))
可見怀估,通過InOrder關(guān)鍵字實現(xiàn)的保序更簡單自然,所以推薦這種方式合搅。其實多搀,關(guān)鍵字InOrder是After的語法糖,源碼如下:
// InOrder declares that the given calls should occur in order.funcInOrder(calls ...*Call){fori :=1; i
當(dāng)mock對象行為的注入保序后灾部,如果行為調(diào)用的順序和其不一致康铭,就會觸發(fā)測試失敗。這就是說赌髓,對于上面的例子从藤,如果在測試用例執(zhí)行過程中,Repository的方法的調(diào)用順序如果不是按 Retrieve -> Create -> Retrieve 的順序進(jìn)行锁蠕,則會導(dǎo)致測試失敗夷野。
mock對象的注入
mock對象的行為都注入到控制器以后,我們接著要將mock對象注入給interface荣倾,使得mock對象在測試中生效悯搔。
在使用GoStub框架之前,很多人都使用土方法舌仍,比如Set鳖孤。這種方法有一個缺陷:當(dāng)測試用例執(zhí)行完成后,并沒有回滾interface到真實對象抡笼,有可能會影響其它測試用例的執(zhí)行。所以黄鳍,筆者強烈建議大家使用GoStub框架完成mock對象的注入推姻。
stubs := StubFunc(&redisrepo.GetInstance, mockDb)deferstubs.Reset()
測試Demo
編寫測試用例有一些基本原則,我們一起回顧一下:
每個測試用例只關(guān)注一個問題框沟,不要寫大而全的測試用例
測試用例是黑盒的
測試用例之間彼此獨立藏古,每個用例要保證自己的前置和后置完備
測試用例要對產(chǎn)品代碼非入侵
...
根據(jù)基本原則增炭,我們不要在一個測試函數(shù)的多個測試用例之間共享mock控制器,于是就有了下面的Demo:
funcTestObjDemo(t *testing.T){? ? Convey("test obj demo", t,func(){? ? ? ? Convey("create obj",func(){? ? ? ? ? ? ctrl := NewController(t)deferctrl.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)deferstubs.Reset()? ? ? ? ? ? ...? ? ? ? })? ? ? ? Convey("bulk create objs",func(){? ? ? ? ? ? ctrl := NewController(t)deferctrl.Finish()? ? ? ? ? ? mockRepo := mock_db.NewMockRepository(ctrl)? ? ? ? ? ? mockRepo.EXPECT().Create(Any(), Any()).Return(nil).Times(5)? ? ? ? ? ? stubs := StubFunc(&redisrepo.GetInstance, mockRepo)deferstubs.Reset()? ? ? ? ? ? ...? ? ? ? })? ? ? ? Convey("bulk retrieve objs",func(){? ? ? ? ? ? ctrl := NewController(t)deferctrl.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)deferstubs.Reset()? ? ? ? ? ? ...? ? ? ? })? ? ? ? ...? ? })}
小結(jié)
本文詳細(xì)闡述了GoMock框架的使用方法拧晕,不但結(jié)合例子給出了標(biāo)準(zhǔn)用法隙姿,而且列出了很多要點,最后通過一個簡單的測試Demo說明了GoConvey + GoStub + GoMock組合使用的正確姿勢厂捞。希望讀者舉一反三输玷,同時將前面三篇的核心內(nèi)容融入進(jìn)來,寫出高質(zhì)量的測試代碼靡馁,最終提升產(chǎn)品質(zhì)量欲鹏。
至此,我們已經(jīng)知道:
全局變量可通過GoStub框架打樁
過程可通過GoStub框架打樁
函數(shù)可通過GoStub框架打樁
interface可通過GoMock框架打樁
于是問題來了臭墨,方法通過神馬打樁赔嚎?我們將在下一篇文章中給出答案。
作者:_張曉龍_
鏈接:http://www.reibang.com/p/f4e773a1b11f
來源:簡書
著作權(quán)歸作者所有胧弛。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán)尤误,非商業(yè)轉(zhuǎn)載請注明出處。