徒手?jǐn)]一個Mock框架(四)—— when XXX then 嘿嘿嘿

徒手?jǐn)]一個Mock框架(一)——如何創(chuàng)建一個mock對象
徒手?jǐn)]一個Mock框架(二)——如何創(chuàng)建final類的代理
徒手?jǐn)]一個Mock框架(三)—— JUnit4Runner+ClassLoader=為所欲為

上一篇我們的StupidMock已經(jīng)解決了創(chuàng)建各種mock對象的問題。今天我們來解決方法調(diào)用mock的問題惑申。

先來看一個例子候学。在使用Mockito的時候藕筋,如果想要mock一個對象的行為,一般的用法是:

when...thenReturn之后梳码,無論原來的方法原本的實現(xiàn)是什么樣子隐圾,如果傳入a,b兩個參數(shù)值,那么就會返回固定的Hello边翁。

今天我們要實現(xiàn)的就是這個東西翎承。

關(guān)鍵點分析

我們先來思考一下,究竟需要做一些什么符匾。從最抽象的程度來說,一個方法調(diào)用可以描述為某個對象調(diào)用一個方法瘩例,參數(shù)是XXX啊胶,最后響應(yīng)是XXX

所以垛贤,我們需要解決四個問題:

  1. 確定對象焰坪;
  2. 確定方法;
  3. 方法調(diào)用參數(shù)聘惦;
  4. 返回值某饰;

如果不考慮用戶體驗的話,我們可以直接讓用戶配置一大堆的東西善绎,把我們所需要的信息都配置過來黔漂,我們傻瓜式的根據(jù)配置跑一下就可以了。

但是在考慮了用戶體驗的時候禀酱,就不能這么做了炬守。

所以,前三個問題也成了很大的問題剂跟。在前面的例子里减途,when接收的是doSomething調(diào)用之后的返回值,所以肯定不能在when方法里面獲取到對象和調(diào)用方法的信息曹洽。于是我們的選擇就只剩下了在調(diào)用doSomething的時候?qū)?nèi)容保存下來鳍置。

而在thenReturn的時候,這個return的內(nèi)容送淆,就直接是受到我們控制的税产,所以很好解決,直接在StupidMock里面保存起來就可以。

于是我們要做的事情就是:

  1. 當(dāng)mock對象調(diào)用某個方法的時候砖第,保存下這次調(diào)用的對象撤卢,參數(shù)信息,以及方法梧兼;
  2. 當(dāng)調(diào)用thenReturn的時候放吩,將參數(shù)也保存下來;
  3. 將前面保存的信息關(guān)聯(lián)起來羽杰,放到一起渡紫。

最終保存的東西,我們稱為stub考赛。

所以當(dāng)用戶發(fā)起一次真的調(diào)用的時候惕澎,我們要做的就是,從所有創(chuàng)建的stub里面颜骤,找到匹配的那個唧喉,將stub中設(shè)置的返回值返回。

獲取對象忍抽、方法和參數(shù)

前面的分析里面提到八孝,我們只能在doSomething方法里面收集對象、方法和參數(shù)鸠项。

現(xiàn)在我們要考慮的問題是:

  1. 如何收集干跛;
  2. 放在哪里,怎么獲人畎怼楼入;

第一個問題理論上來說,并不復(fù)雜牧抽,因為我們創(chuàng)建的mock對象嘉熊,是利用cglib來創(chuàng)建的,我們可以在創(chuàng)建代理的時候阎姥,傳入callback參數(shù)记舆,這個callback就是用來保存這一次的調(diào)用對象、方法和參數(shù)呼巴;

第二個問題泽腮,更加多的是設(shè)計的問題。我們可以直接把這些信息放在mock對象內(nèi)部衣赶,然后在when方法里面將它取出來诊赊。有一個問題是,我們無法區(qū)別兩種調(diào)用府瞄,即無法區(qū)別用戶是在創(chuàng)建一個stub還是真的在執(zhí)行一個調(diào)用碧磅。

解決辦法就是碘箍,我們都處理。既認為這是一次調(diào)用鲸郊,也認為這是一個創(chuàng)建stub丰榴。

  1. 作為一次調(diào)用,我們將從所有已經(jīng)注冊的stub里面找到匹配的秆撮,返回注冊的返回值四濒;
  2. 作為一次創(chuàng)建stub的步驟之一,我們將保存這次調(diào)用的上下文职辨;

為了統(tǒng)一處理盗蟆,我們會在創(chuàng)建mock對象的時候,加入一個默認的stub舒裤,該stub就是各種類型的默認值喳资。如基本類型則是基本類型對應(yīng)的默認值,如果是對象則返回null腾供。

Callback實現(xiàn)

上圖是我們的mock對象時候使用的方法仆邓,很容易發(fā)現(xiàn),關(guān)鍵點就在于實現(xiàn)MethodInterceptor接口台腥,并且注冊進去宏赘。

所以我們先實現(xiàn)一個自己的MethodInterceptor

MethodInterceptor的實現(xiàn)關(guān)鍵是MockObjectSkeletonThreadSafeStubBuilder黎侈。本質(zhì)上來說,這個實現(xiàn)只是一個“膠合層”闷游,負責(zé)將StupidMockcglib粘在一起峻汉。雖然理論上來說,我可以將MockObjectSkeletonThreadSafeBuilder的邏輯都直接寫在其中脐往,但是這會讓我們的實現(xiàn)過于臃腫休吠。

這里還有一個將Object轉(zhuǎn)化為ArgMatcher的過程。這是因為业簿,在我們的stub里面瘤礁,并不能直接使用這個參數(shù),而是要保存一些參數(shù)匹配條件梅尤。

比如說有些時候我們的寫法可能是:when(obj.doSomething(any(),any()).then(...)柜思。

于是我們對應(yīng)的StupidMock就變成了:

最終的使用效果類似:

MockObjectSkeleton

MockObjectSkeleton在這里更加接近一個容器的概念。它里面負責(zé)放置stub實例巷燥,并且從stub里面找出一個來赡盘,執(zhí)行stub,并返回對象缰揪。

這里有一個地方需要注意的是陨享,我采用的是一個List來保存stub。并且每次添加的時候都是將stub加在隊列前。

這是一個非常粗糙的做法:

  1. 按照我們的匹配原則抛姑,如果我們設(shè)定了兩個stub赞厕,對于某一次方法調(diào)用,那么后一個設(shè)定的stub就會覆蓋掉前一個定硝,作為結(jié)果返回皿桑;
  2. 對于一個方法來說,可以有很多stub喷斋,并且我們沒有提供刪除某些stub的方法唁毒;

可以考慮用一個Map結(jié)構(gòu)來取代List,以實現(xiàn)單個方法只會有一個stub星爪。

StubBuilder

StubBuilder則是另外一個關(guān)鍵點浆西。上圖的接口定義其實很好理解,需要額外解釋的就是addOberver方法顽腾。

這是一個觀察者模式的應(yīng)用近零。它主要是為了解決MockObjectSkeleton需要維護stub,而創(chuàng)建stub則是在StubBuilder里面完成的抄肖。除此以外久信,一種可取得做法我們可以將MockObjectSkeleton的實例傳入StubBuilder實例,但是這意味著兩者將強耦合在一起漓摩,這是我所不希望的裙士。所以設(shè)計了一個BuildingStubObserver接口,單純就是為了解耦管毙,以及擴展性腿椎。

現(xiàn)在要來看最為繞的地方了,就是ThreadSafeStubBuilder夭咬。在此之前啃炸,我要先分析一下我們面對的困難時什么。

在我們的模型里面卓舵,牽涉到了simpleObject——即mock對象南用,StupidMockMethodInterceptorAdaptorImpl實例——在創(chuàng)建mock對象的時候創(chuàng)建,StupidMock——它的靜態(tài)方法掏湾,還有核心stub實例裹虫。

這意味著,我們需要在這些所有牽涉到的對象或者類中共享stub實例的創(chuàng)建過程忘巧。我們要在StupidMockMethodInterceptorAdaptorImpl里面創(chuàng)建StubBuilder并且這個StubBuilder要在StupidMock里面被返回恒界。

于是關(guān)鍵問題是,StupidMockMethodInterceptorAdaptorImpl怎么把StubBuilder傳遞給StupidMock砚嘴。

答案是通過某個共享的中間變量十酣。

這個共享中間變量就是ThreadSafeStubBuilder涩拙。

實際上,我們是利用了ThreadSafeStubBuilder里面的靜態(tài)變量stubBuilder來實現(xiàn)這種共享的耸采。stubBuilder利用了Java的ThreadLocal特性兴泥,來保證線程安全。

所以虾宇,無論是在StupidMockMethodInterceptorAdaptorImpl里面new ThreadSafeStubBuilder還是在StupidMock里面new ThreadSafeStubBuilder搓彻,它們實際上操作的都是同一個StubBuilder

IStub, Answer和ArgMatcher

IStub的定義只有兩個方法嘱朽,一個是判斷自身與某一次實際調(diào)用是否匹配旭贬,如果匹配的話,則意味著要使用該stub實例搪泳,于是調(diào)用getAnswer得到answer實例.

Answer接口被定義為函數(shù)式接口稀轨,里面只有一個方法。它代表的就是用戶想要在實際調(diào)用時候mock的動作岸军。

IStub的默認實現(xiàn)DefaultStubImpl之中奋刽,match方法的實現(xiàn)如下:

其邏輯最重要的部分就是參數(shù)匹配,這是利用ArgMatcher來進行的:

注意到的是艰赞,在StupidMockMethodInterceptorAdaptorImpl里面我們只使用了一種實現(xiàn)佣谐,就是FixedValueArgMatcherImpl。因為這一篇文章不討論復(fù)雜的參數(shù)匹配問題方妖,我會在下一篇討論這個問題狭魂。FixedValueArgMatcherImpl就是指匹配特定值,其實現(xiàn)是:

設(shè)計總結(jié)

這一篇文章党觅,其實沒有涉及太多復(fù)雜的技術(shù)趁蕊,更加多的是設(shè)計上的問題。我在弄這個東西的時候仔役,很多時候都抄襲了Mockito的東西,不過將里面復(fù)雜的東西都去掉了是己。

但是核心問題又兵,或者說,關(guān)鍵點卒废,我自認為還是保留下來了沛厨。

這個核心問題就是我所談及的,如果讓StubBuilder在各個地方共享摔认,并且能夠保證線程安全逆皮,以及mock的正確性。

現(xiàn)在我來列舉一下這個設(shè)計的核心接口参袱。這些接口定義了整個系統(tǒng)的運作方式电谣,堪稱靈魂秽梅。

第一個接口是IStub接口。定義了一個stub應(yīng)該知曉自己是否能夠被某次調(diào)用所使用剿牺,并且定義了該如何“應(yīng)答”這次調(diào)用企垦。

這就是Answer接口。Answer接口解決了在mock中做什么的問題晒来。在Mockito里面那些復(fù)雜then, thenReturn, thenThrow之類的钞诡,都可以實現(xiàn)Answer接口以達成。

而另外一個接口StubBuilder接口湃崩,則定義了一個stub該如何被創(chuàng)建出來荧降。它是將cglibmock對象攒读,和StupidMock以及其余(后續(xù)會有)東西結(jié)合起來的關(guān)鍵朵诫。

至于剩下的東西,不過是一些邊角之物整陌。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末拗窃,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子泌辫,更是在濱河造成了極大的恐慌随夸,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件震放,死亡現(xiàn)場離奇詭異宾毒,居然都是意外死亡,警方通過查閱死者的電腦和手機殿遂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門诈铛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人墨礁,你說我怎么就攤上這事幢竹。” “怎么了恩静?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵焕毫,是天一觀的道長。 經(jīng)常有香客問我驶乾,道長邑飒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任级乐,我火速辦了婚禮疙咸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘风科。我一直安慰自己撒轮,他們只是感情好乞旦,可當(dāng)我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著腔召,像睡著了一般杆查。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上臀蛛,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天亲桦,我揣著相機與錄音,去河邊找鬼浊仆。 笑死客峭,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的抡柿。 我是一名探鬼主播舔琅,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼洲劣!你這毒婦竟也來了备蚓?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤囱稽,失蹤者是張志新(化名)和其女友劉穎郊尝,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體战惊,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡流昏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了吞获。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片况凉。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖各拷,靈堂內(nèi)的尸體忽然破棺而出刁绒,到底是詐尸還是另有隱情,我是刑警寧澤烤黍,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布膛锭,位于F島的核電站,受9級特大地震影響蚊荣,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜莫杈,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一互例、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧筝闹,春花似錦媳叨、人聲如沸腥光。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽武福。三九已至,卻和暖如春痘番,著一層夾襖步出監(jiān)牢的瞬間捉片,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工汞舱, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留伍纫,地道東北人。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓昂芜,卻偏偏與公主長得像莹规,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子泌神,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,077評論 2 355

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

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,105評論 1 32
  • 什么是單元測試良漱? 單元測試(又稱為模塊測試, Unit Testing)是針對程序模塊(軟件設(shè)計的最小單位)來進行...
    常曉csc閱讀 9,395評論 0 6
  • 自從老夫換了一個新廠之后,單測就寫個不停欢际,因為新廠對單測的要求還是比較高的母市。 在擼單測的過程中,用Mockito,...
    flycash閱讀 3,686評論 1 6
  • 單元測試的目標(biāo)和挑戰(zhàn) 單元測試的思路是在不涉及依賴關(guān)系的情況下測試代碼(隔離性)幼苛,所以測試代碼與其他類或者系統(tǒng)的關(guān)...
    jiangmo閱讀 2,133評論 0 2
  • Mockito簡介什么是mock窒篱?在軟件開發(fā)的世界之外, "mock"一詞是指模仿或者效仿。 因此可以將“mock...
    燕京博士閱讀 3,552評論 0 6