徒手?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。
所以垛贤,我們需要解決四個問題:
- 確定對象焰坪;
- 確定方法;
- 方法調(diào)用參數(shù)聘惦;
- 返回值某饰;
如果不考慮用戶體驗的話,我們可以直接讓用戶配置一大堆的東西善绎,把我們所需要的信息都配置過來黔漂,我們傻瓜式的根據(jù)配置跑一下就可以了。
但是在考慮了用戶體驗的時候禀酱,就不能這么做了炬守。
所以,前三個問題也成了很大的問題剂跟。在前面的例子里减途,when
接收的是doSomething
調(diào)用之后的返回值,所以肯定不能在when
方法里面獲取到對象和調(diào)用方法的信息曹洽。于是我們的選擇就只剩下了在調(diào)用doSomething
的時候?qū)?nèi)容保存下來鳍置。
而在thenReturn
的時候,這個return
的內(nèi)容送淆,就直接是受到我們控制的税产,所以很好解決,直接在StupidMock
里面保存起來就可以。
于是我們要做的事情就是:
- 當(dāng)
mock
對象調(diào)用某個方法的時候砖第,保存下這次調(diào)用的對象撤卢,參數(shù)信息,以及方法梧兼; - 當(dāng)調(diào)用
thenReturn
的時候放吩,將參數(shù)也保存下來; - 將前面保存的信息關(guān)聯(lián)起來羽杰,放到一起渡紫。
最終保存的東西,我們稱為stub
考赛。
所以當(dāng)用戶發(fā)起一次真的調(diào)用的時候惕澎,我們要做的就是,從所有創(chuàng)建的stub
里面颜骤,找到匹配的那個唧喉,將stub
中設(shè)置的返回值返回。
獲取對象忍抽、方法和參數(shù)
前面的分析里面提到八孝,我們只能在doSomething
方法里面收集對象、方法和參數(shù)鸠项。
現(xiàn)在我們要考慮的問題是:
- 如何收集干跛;
- 放在哪里,怎么獲人畎怼楼入;
第一個問題理論上來說,并不復(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丰榴。
- 作為一次調(diào)用,我們將從所有已經(jīng)注冊的
stub
里面找到匹配的秆撮,返回注冊的返回值四濒; - 作為一次創(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)鍵是MockObjectSkeleton
和ThreadSafeStubBuilder
黎侈。本質(zhì)上來說,這個實現(xiàn)只是一個“膠合層”闷游,負責(zé)將StupidMock
和cglib
粘在一起峻汉。雖然理論上來說,我可以將MockObjectSkeleton
和ThreadSafeBuilder
的邏輯都直接寫在其中脐往,但是這會讓我們的實現(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
加在隊列前。
這是一個非常粗糙的做法:
- 按照我們的匹配原則抛姑,如果我們設(shè)定了兩個
stub
赞厕,對于某一次方法調(diào)用,那么后一個設(shè)定的stub
就會覆蓋掉前一個定硝,作為結(jié)果返回皿桑; - 對于一個方法來說,可以有很多
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)建出來荧降。它是將cglib
,mock
對象攒读,和StupidMock
以及其余(后續(xù)會有)東西結(jié)合起來的關(guān)鍵朵诫。
至于剩下的東西,不過是一些邊角之物整陌。