GoMock框架使用指南

序言

要寫出好的測試代碼浮还,必須精通相關(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)載請注明出處。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末结缚,一起剝皮案震驚了整個濱河市损晤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌掺冠,老刑警劉巖沉馆,帶你破解...
    沈念sama閱讀 212,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異德崭,居然都是意外死亡斥黑,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評論 3 385
  • 文/潘曉璐 我一進(jìn)店門眉厨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來锌奴,“玉大人,你說我怎么就攤上這事憾股÷故瘢” “怎么了?”我有些...
    開封第一講書人閱讀 158,369評論 0 348
  • 文/不壞的土叔 我叫張陵服球,是天一觀的道長茴恰。 經(jīng)常有香客問我,道長斩熊,這世上最難降的妖魔是什么往枣? 我笑而不...
    開封第一講書人閱讀 56,799評論 1 285
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上分冈,老公的妹妹穿的比我還像新娘圾另。我一直安慰自己,他們只是感情好雕沉,可當(dāng)我...
    茶點故事閱讀 65,910評論 6 386
  • 文/花漫 我一把揭開白布集乔。 她就那樣靜靜地躺著,像睡著了一般坡椒。 火紅的嫁衣襯著肌膚如雪扰路。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,096評論 1 291
  • 那天肠牲,我揣著相機與錄音幼衰,去河邊找鬼。 笑死缀雳,一個胖子當(dāng)著我的面吹牛渡嚣,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播肥印,決...
    沈念sama閱讀 39,159評論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼识椰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了深碱?” 一聲冷哼從身側(cè)響起腹鹉,我...
    開封第一講書人閱讀 37,917評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎敷硅,沒想到半個月后功咒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,360評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡绞蹦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,673評論 2 327
  • 正文 我和宋清朗相戀三年力奋,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片幽七。...
    茶點故事閱讀 38,814評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡景殷,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出澡屡,到底是詐尸還是另有隱情猿挚,我是刑警寧澤,帶...
    沈念sama閱讀 34,509評論 4 334
  • 正文 年R本政府宣布驶鹉,位于F島的核電站绩蜻,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏室埋。R本人自食惡果不足惜辜羊,卻給世界環(huán)境...
    茶點故事閱讀 40,156評論 3 317
  • 文/蒙蒙 一踏兜、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧八秃,春花似錦、人聲如沸肉盹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽上忍。三九已至骤肛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間窍蓝,已是汗流浹背腋颠。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留吓笙,地道東北人淑玫。 一個月前我還...
    沈念sama閱讀 46,641評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像面睛,于是被迫代替她去往敵國和親絮蒿。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,728評論 2 351

推薦閱讀更多精彩內(nèi)容

  • 序言 要寫出好的測試代碼叁鉴,必須精通相關(guān)的測試框架土涝。對于Golang的程序員來說,至少需要掌握下面四個測試框架: G...
    _張曉龍_閱讀 38,851評論 3 48
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理幌墓,服務(wù)發(fā)現(xiàn)但壮,斷路器,智...
    卡卡羅2017閱讀 134,638評論 18 139
  • 1.Creating mock objects 1.1Class mocks idclassMock=OCMCla...
    奔跑的小小魚閱讀 2,583評論 0 0
  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉常侣,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,692評論 0 9
  • 01 一切從一則新聞?wù)f起袭祟。 今天看到以下這句話验残,一個小學(xué)生說: 當(dāng)我孤單的時候,我媽把手機給了我巾乳。 孩子怎么了您没?孤...
    蕭筱筱閱讀 340評論 7 12