測試驅動的開發(Test Driven Design, TDD)要求咱們先寫單元測試,再寫實現代碼。在寫單元測試的過程當中,一個很廣泛的問題是,要測試的類會有不少依賴,這些依賴的類/對象/資源又會有別的依賴,從而造成一個大的依賴樹,要在單元測試的環境中完整地構建這樣的依賴,是一件很困難的事情。
所幸,咱們有一個應對這個問題的辦法:Mock。簡單地說就是對測試的類所依賴的其餘類和對象,進行mock - 構建它們的一個假的對象,定義這些假對象上的行爲,而後提供給被測試對象使用。被測試對象像使用真的對象同樣使用它們。用這種方式,咱們能夠把測試的目標限定於被測試對象自己,就如同在被測試對象周圍作了一個劃斷,造成了一個儘可能小的被測試目標。Mock的框架有不少,最爲知名的一個是Mockito,這是一個開源項目,使用普遍。官網:http://site.mockito.org/。java
首先咱們要知道,Mock對象這件事情,本質上是一個Proxy模式的應用。Proxy模式說的是,在一個真實對象前面,提供一個proxy對象,全部對真實對象的調用,都先通過proxy對象,而後由proxy對象根據狀況,決定相應的處理,它能夠直接作一個本身的處理,也能夠再調用真實對象對應的方法。示例:安全
代碼中的註釋描述了代碼的邏輯:先建立mock對象,而後設置mock對象上的方法get,指定當get方法被調用,而且參數爲0的時候,返回」one」;而後,調用被測試方法(被測試方法會調用mock對象的get方法);最後進行驗證。邏輯很好理解,可是初次看到這個代碼的人,會以爲有點兒奇怪,總感受這個代碼跟通常的代碼不太同樣。讓咱們仔細想一想看,下面這個代碼:bash
// 設置mock對象的行爲 - 當調用其get方法獲取第0個元素時,返回」one」
Mockito.when(mockedList.get(0)).thenReturn(「one」);框架
public class MockDemo { // 建立mock對象 List<String> mockedList = Mockito.mock(List.class); @Before public void setUp(){ // 設置mock對象的行爲 - 當調用其get方法獲取第0個元素時,返回"one" Mockito.when(mockedList.get(0)).thenReturn("one"); } @Test public void mockDemoTest(){ // 使用mock對象 - 會返回前面設置好的值"one",即使列表其實是空的 String str = mockedList.get(0); Assert.assertTrue("one".equals(str)); Assert.assertTrue(mockedList.size() == 0); } }
若是按照通常代碼的思路去理解,是要作這麼一件事:調用mockedList.get方法,傳入0做爲參數,而後獲得其返回值(一個object),而後再把這個返回值傳給when方法,而後針對when方法的返回值,調用thenReturn。好像有點不通?mockedList.get(0)的結果,語義上是mockedList的一個元素,這個元素傳給when是表示什麼意思?因此,咱們不能按照尋常的思路去理解這段代碼。實際上這段代碼要作的是描述這麼一件事情:當mockedList的get方法被調用,而且參數的值是0的時候,返回」one」。很不尋常,對嗎?若是用日常的面向對象的思想來設計API來作一樣的事情,估計結果是這樣的:函數
Mockito.returnValueWhen(「one」, mockedList, 「get」, 0);
第一個參數描述要返回的結果,第二個參數指定mock對象,第三個參數指定mock方法,後面的參數指定mock方法的參數值。這樣的代碼,更符合咱們看通常代碼時候的思路。單元測試
可是,把上面的代碼跟Mockito的代碼進行比較,咱們會發現,咱們的代碼有幾個問題:
1.不夠直觀
2.對重構不友好
第二點尤爲重要。想象一下,若是咱們要作重構,把get方法更名叫fetch方法,那咱們要把」get」字符串替換成」fetch」,而字符串替換沒有編譯器的支持,須要手工去作,或者查找替換,很容易出錯。而Mockito使用的是方法調用,對方法的更名,能夠用編譯器支持的重構來進行,更加方即可靠。測試
明確了Mockito的方案更好以後,咱們來看看Mockito的方案是如何實現的。首先咱們要知道,Mock對象這件事情,本質上是一個Proxy模式的應用。Proxy模式說的是,在一個真實對象前面,提供一個proxy對象,全部對真實對象的調用,都先通過proxy對象,而後由proxy對象根據狀況,決定相應的處理,它能夠直接作一個本身的處理,也能夠再調用真實對象對應的方法。Proxy對象對調用者來講,能夠是透明的,也能夠是不透明的。fetch
Java自己提供了構建Proxy對象的API:Java Dynamic Proxy API,而Mockito是用Cglib來實現的。
下面看下運行時期Cglib生成的Mock代理對象的.class文件是怎麼樣的this
public class List$$EnhancerByMockitoWithCGLIB$$d85c0201 implements List, Factory { ........ private static final Method CGLIB$get$9$Method; ........ public final boolean removeAll(Collection var1) { MethodInterceptor var10000 = this.CGLIB$CALLBACK_0; if(this.CGLIB$CALLBACK_0 == null) { CGLIB$BIND_CALLBACKS(this); var10000 = this.CGLIB$CALLBACK_0; } if(var10000 != null) { Object var2 = var10000.intercept(this, CGLIB$removeAll$26$Method, new Object[]{var1}, CGLIB$removeAll$26$Proxy); return var2 == null?false:((Boolean)var2).booleanValue(); } else { return super.removeAll(var1); } } final boolean CGLIB$retainAll$27(Collection var1) { return super.retainAll(var1); } .......... case -208030418: if(var10000.equals("get(I)Ljava/lang/Object;")) { return CGLIB$get$9$Proxy; } break; ......... }
能夠看到Mokito利用Cglib爲List的全部方法都作了Mock實現,可是咱們只對get方法作了Stub,因此只用關注這些代碼spa
CGLIB$get$9$Proxy = MethodProxy.create(var1, var0, "(I)Ljava/lang/Object;", "get", "CGLIB$get$9"); case -208030418: if(var10000.equals("get(I)Ljava/lang/Object;")) { return CGLIB$get$9$Proxy; } break;
看到第一句是否是和我上面說的面向對象的寫法很像
下面咱們來看看,到底如何實現文章開頭的示例中的API。若是咱們仔細分析,就會發現,示例代碼最難理解的部分是創建Mock對象(proxy對象),並配置好mock方法(指定其在什麼狀況下返回什麼值)。只要設置好了這些信息,後續的驗證是比較容易理解的,由於全部的方法調用都通過了proxy對象,proxy對象能夠記錄全部調用的信息,供驗證的時候去檢查。下面咱們重點關注stub配置的部分,也就是咱們前面提到過的這一句代碼:
// 設置mock對象的行爲 - 當調用其get方法獲取第0個元素時,返回"one" Mockito.when(mockedList.get(0)).thenReturn("one");
當when方法被調用的時候,它其實是沒有辦法獲取到mockedList上調用的方法的名字(get),也沒有辦法獲取到調用時候的參數(0),它只能得到mockedList.get方法調用後的返回值,而根本沒法知道這個返回值是經過什麼過程獲得的。這就是普通的java代碼。爲了驗證咱們的想法,咱們實際上能夠把它重構成下面的樣子,不改變它的功能:
// 設置mock對象的行爲 - 當調用其get方法獲取第0個元素時,返回"one" String str = mockedList.get(0); Mockito.when(str).thenReturn("one");
這對Java開發者來講是常識,那麼這個常識對Mockito是否還有效呢。咱們把上面的代碼放到Mockito測試中實際跑一遍,結果跟前面的寫法是同樣的,證實了常識依然有效。
有了上面的分析,咱們基本上能夠猜出來Mockito是使用什麼方式來傳遞信息了 —— 不是用方法的返回值,而是用某種全局的變量。當get方法被調用的時候(調用的其實是proxy對象的get方法),代碼實際上保存了被調用的方法名(get),以及調用時候傳遞的參數(0),而後等到thenReturn方法被調用的時候,再把」one」保存起來,這樣,就有了構建一個stub方法所需的全部信息,就能夠構建一個stub方法了。
上面的設想是否正確呢?Mockito是開源項目,咱們能夠從代碼當中驗證咱們的想法。下面是MockHandlerImpl.handle()方法的代碼。代碼來自Mockito在Github上的代碼。
public Object handle(Invocation invocation) throws Throwable {
if (invocationContainerImpl.hasAnswersForStubbing()) { ... } ... InvocationMatcher invocationMatcher = matchersBinder.bindMatchers( mockingProgress.getArgumentMatcherStorage(), invocation ); mockingProgress.validateState(); // if verificationMode is not null then someone is doing verify() if (verificationMode != null) { ... } // prepare invocation for stubbing invocationContainerImpl.setInvocationForPotentialStubbing(invocationMatcher); OngoingStubbingImpl<T> ongoingStubbing = new OngoingStubbingImpl<T>(invocationContainerImpl); mockingProgress.reportOngoingStubbing(ongoingStubbing); ... }
注意第1行,第6-9行,能夠看到方法調用的信息(invocation)對象被用來構造invocationMatcher對象,而後在第19-21行,invocationMatcher對象最終傳遞給了ongoingStubbing對象。完成了stub信息的保存。這裏咱們忽略了thenReturn部分的處理。有興趣的同窗能夠本身看代碼研究。
看到這裏,咱們能夠得出結論,mockedList對象的get方法的實際處理函數是一個proxy對象的方法(最終調用MockHandlerImpl.handle方法),這個handle方法除了return返回值以外,還作了大量的處理,保存了stub方法的調用信息,以便以後能夠構建stub。
經過以上的分析咱們能夠看到,Mockito在設計時實際上有意地使用了方法的「反作用」,在返回值以外,還保存了方法調用的信息,進而在最後利用這些信息,構建出一個mock。而這些信息的保存,是對Mockito的用戶徹底透明的。「模式」告訴咱們,在設計方法的時候,應該避免反作用,一個方法在被調用時候,除了return返回值以外,不該該產生其餘的狀態改變,尤爲不該該有「意料以外」的改變。但Mockito徹底違反了這個原則,Mockito的靜態方法Mockito.anyString(), mockInstance.method(), Mockito.when(), thenReturn(),這些方法,在背後都有很大的「反作用」 —— 保存了調用者的信息,而後利用這些信息去完成任務。這就是爲何Mockito的代碼一開始會讓人以爲奇怪的緣由,由於咱們平時不這樣寫代碼。
然而,做爲一個Mocking框架,這個「反模式」的應用其實是一個好的設計。就像咱們前面看到的,它帶來了很是簡單的API,以及編譯安全,可重構等優良特性。違反直覺的方法調用,在明白其原理和一段時間的熟悉以後,也顯得很是的天然了。設計的原則,終究是爲設計目標服務的,原則在總結出來以後,不該該成爲僵硬的教條,根據需求靈活地應用這些原則,才能達成好的設計。在這方面,Mockito堪稱一個經典案例。