一、百變怪 Mockito
Mockito可謂是Java世界的百變怪,使用它,可以輕易的復制出各種類型的對象侦副,并與之進行交互。
1.1 對象“復制”
// 列表
List mockList = mock(List.class);
mockList.add(1);
mockList.clear();
// Socket對象
Socket mockSocket = mock(Socket);
mockSocket.connect(new InetSocketAddress(8080));
mockSocket.close();
1.2 技能復制
List mockList = mock(List.class);
mockList.add(1); // 簡單交互
mockList.get(1); // 返回值為null
mockList.size(); // 返回值為0
雖然復制出來的對象上的所有方法都能被調(diào)用驼鞭,但好像這個百變怪的技能有點弱呢...
其實秦驯,是使用方式不對,這個百變怪掌握的僅僅是基礎技能挣棕,對于有返回值的調(diào)用译隘,只會返回默認的返回值,在需要返回對象的場合洛心,返回null
固耘,需要返回int
的場合,返回0
词身。其他的默認返回值厅目,見下表:
// todo 默認返回值表
要讓它能按我們的需要展現(xiàn)技能(方法),需要事先“教會”它法严。
List mockList = mock(List.class);
when(mockList.get(anyInt()).thenReturn(1);
when(mockList.size()).thenReturn(1, 2, 3);
assertEquals("預期返回1", 1, mockList.get(1)); // pass
assertEquals("預期返回1", 1, mockList.get(2)); // pass
assertEquals("預期返回1", 1, mockList.get(3)); // pass
assertEquals("預期返回1", 1, mockList.size()); // pass
assertEquals("預期返回2", 2, mockList.size()); // pass
assertEquals("預期返回3", 3, mockList.size()); // pass
上面的代碼损敷,我們教會了這個百變怪:
- 只要調(diào)用
get
方法,不管參數(shù)是什么深啤,都返回1
拗馒; - 對于
size
方法調(diào)用,第一次返回1
溯街,第二次調(diào)用返回2
诱桂,第三次開始,則返回3
呈昔。
是的挥等,這個百變怪就是這么的笨,只會有樣學樣堤尾「尉ⅲ看起來一點用都沒有。
1.3 驗證
但是呢哀峻,雖然它笨涡相,但是它卻具備一些“笨方法”,也不算沒有用剩蟀。
verify(mockList, never()).clear(); // 從未調(diào)用過clear方法
verify(mockList, times(2)).get(1); // get(1)方法調(diào)用了2次
verify(mockList, times(3)).get(anyInt()); // get(任意數(shù)字)調(diào)用了3次
verfiy(mockList, times(4)).size(); // 這里會失敗催蝗,因為上面我們只調(diào)用了size方法3次
可以看到,這個百變怪雖然笨育特,但不傻丙号,對于它自己做過了什么,它是記得一清二楚的缰冤。至于它還有什么其他技能犬缨,可以到官網(wǎng)看下他的使用說明書詳細了解。
1.4 小結
可以看到棉浸,雖然Mockito在正式的場合(生產(chǎn)環(huán)境)下派不上什么用場怀薛,但在訓練場(測試環(huán)境)上,卻能夠成為一個相當不錯的陪練迷郑。
所以枝恋,Mockito是一個適用于單元測試的mock庫。在單元測試中嗡害,可以通過它來方便的生成模擬對象焚碌。便于進行測試。
二霸妹、Mockito與單元測試
2.1 例
假設我們有一段業(yè)務邏輯十电,需要對給定的請求做處理,在這種情況下叹螟,倘若要手工構造發(fā)起一個請求鹃骂,那想必是很麻煩蛋疼。首先我們需要把代碼編譯部署到測試服務器上罢绽,然后構造并發(fā)起一個請求偎漫,等待服務器接收到請求后,交給我們的業(yè)務進行處理有缆。如下:
// 業(yè)務代碼
public boolean handleRequest(HttpServletRequest request) {
String module = request.getParameter("module");
if ("live".equals(module)) {
// handle module live request
return true;
} else if ("user".equals(module)) {
// handle module user request
return true;
}
return false;
}
為了測試這么一點點代碼象踊,就需要我們額外付出那么多的操作,對于追求效率的程序員來說棚壁,這種重復操作&等待簡直就是慢性自殺杯矩。這里的代碼還是相對簡單的,要是請求的內(nèi)容更加復雜袖外,難道還要花上大把時間研究如何構造出這么一個Http請求嗎史隆?
其實,測試這段邏輯曼验,我們想要做的事情其實很簡單泌射,給定一個特定的輸入粘姜,驗證其輸出結果是否正確。也就是熔酷,驗證的過程孤紧,應該盡可能的簡單方便,把大部分的時間耗費在驗證過程上絕對是有問題的拒秘。
如果我們使用單元測試号显,搭配Mockito,完全可以寫出如下測試躺酒,在代碼提交之前押蚤,先在本地的JVM上過一遍測試。
@Test
public void handleRequestTestLive() throws Exception {
HttpServletRequest request = mock(HttpServletRequest);
when(request.getParameter("module")).thenReturn("live");
boolean ret = handleRequest(request);
assertEquals(true, ret)
}
@Test
public void handleRequestTestUser() throws Exception {
HttpServletRequest request = mock(HttpServletRequest);
when(request.getParameter("module")).thenReturn("user");
boolean ret = handleRequest(request);
assertEquals(true, ret)
}
@Test
public void handleRequestTestNone() throws Exception {
HttpServletRequest request = mock(HttpServletRequest);
when(request.getParameter("module")).thenReturn(null);
boolean ret = handleRequest(request);
assertEquals(false, ret)
}
首先羹应,我們模擬出一個假對象揽碘,并設定這個假對象的行為,這個假對象的行為會影響我們業(yè)務邏輯的結果园匹,所以我們可以在不同的測試用例里钾菊,設定假對象返回不同的行為,這樣我們就能驗證各種輸入下偎肃,我們的業(yè)務邏輯是不是能夠按我們的設想正常工作煞烫。
2.2 Mockito 原理剖析
Ok,到現(xiàn)在為止累颂,我們通過幾個例子簡單的展示了Mockito滞详,以及它在單元測試中起到作用。從例子中可以看到紊馏,Mockito的使用是很直觀的料饥,使用起來行云流水,就跟說話一樣自然朱监。某種程度上岸啡,也可以看做代碼即注釋
的一種表現(xiàn)。當然這有點扯遠了赫编。
Mockito的這種神乎其技的使用方式巡蘸,使得我在一開始見到它的時候,感到驚訝擂送,驚訝之余又感到不解悦荒。
如mock(List.class)
,怎么就能夠從List.class
這個接口搞出一個可以用的對象嘹吨?when(mockList.size()).thenReturn(20)
這種搬味,竟然就能干預到mock對象的執(zhí)行,插樁返回了20。mockList.size()
本身不就是一個方法調(diào)用嗎碰纬?verify(mockList, never()).add(10)
萍聊,這種驗證方式又是通過什么黑科技實現(xiàn)的?悦析?寿桨?
看著Mockito
的使用文檔的我,當時真是一臉黑人問號她按。
后來,從我有限的知識儲備里炕柔,我想到了mock
的實現(xiàn)方式可能是使用泛型 + 動態(tài)代理
實現(xiàn)酌泰,當想到這種組合的時候,我不禁感慨庫作者的思維的精妙匕累,所以我決定研究下Mockito的源碼陵刹,看看作者是怎么做到的。
當然欢嘿,后來我發(fā)現(xiàn)衰琐,泛型是用到了(廢話),動態(tài)代理技術卻沒有用到炼蹦。好了羡宙,閑話不多說,下面來講講Mockito
的實現(xiàn)掐隐。由于在座同學狗热,平時使用Java應該不多,所以這里我就不深入講解細節(jié)虑省,會比較偏向原理性的東西匿刮。
2.3 Mock
讓我們來分析一下,要mock一個對象探颈,我們需要做什么熟丸。
- 首先需要知道要Mock的對象的類型,這樣我們才能生成這個類型的對象
- 為了生成這個類型的對象伪节,那么這個類型需要是能實例化的光羞,但如果這個類型是抽象類或者一個接口?要怎么辦怀大?我們知道狞山,抽象類和接口需要被實現(xiàn),才能實例化叉寂,因此萍启,最自然的方式就是,繼承自這個類型,然后給這些方法一個空實現(xiàn)勘纯。
- 有了可以實例化的類型局服,接下來就好辦了:實例化這個類型,并上轉(zhuǎn)型成我們的目標類驳遵,返回淫奔。
總結起來就是:給到要mock的類型、生成一個繼承這個類型的類堤结、實例化生成的類唆迁、得到mock對象。
Mockito的源碼里正是這么做的:
- 暴露出Mockito.mock接口給使用者
- 得到要mock的類型竞穷,進行一些設置唐责,然后一路傳遞到
SubclassBytecodeGenerator
,由它來生成mock類型的子類 - 得到這個類型后瘾带,
SubclassByteBuddyMockMaker
將其實例化
第二步的實現(xiàn)借助了ByteBuddy
這個框架鼠哥,這個框架可以直接生成Java的類,然后通過ClassLoader加載進來使用看政。這里就不深入了朴恳。
第三步實例化,實例化使用了objenesis
允蚣,一個能在不同平臺上實例化一個類的庫于颖。
經(jīng)過這幾步,就得到了一個可以用來操作的模擬對象嚷兔。
實現(xiàn)的思路大致是這樣恍飘,代碼里的處理還有很多細節(jié)性的部分,這里不進行源碼探究谴垫,就不多講了
2.4 打樁
when這一步要實現(xiàn)的功能是打樁章母。
那么,對于when(mockType.someMethod()).thenReturn(value)
這樣的方法調(diào)用翩剪,該怎么實現(xiàn)乳怎?
一開始我以為方法調(diào)用的返回值有貓膩,返回值唯一標識一次方法調(diào)用前弯,通過在內(nèi)部記錄這個值蚪缀,來返回特定的值。但對于每個方法調(diào)用恕出,返回一個特定的返回值并不可能询枚,何況有的方法調(diào)用并沒有返回值。
這個功能Mockito是這么實現(xiàn)的:
在mock那一步浙巫,我們知道了Mockito生成了一個派生類金蜀,派生類里的所有方法調(diào)用刷后,也已經(jīng)被hook掉,即所有的方法調(diào)用渊抄,并不會執(zhí)行到原有的實現(xiàn)邏輯里尝胆,而是會返回一個默認值。
所有的方法調(diào)用最終都會交由MockHandlerImpl.handle
來執(zhí)行护桦。這個類很重要含衔,可以說是Mockito整個功能的核心所在。
在進行方法調(diào)用的時候二庵,Mockito會假定這個方法調(diào)用需要被打樁贪染,生成一個和這個方法調(diào)用相對應的OngoingStubbing
對象,將這個對象暫時存起來催享。
當when
方法執(zhí)行的時候杭隙,就會取出這個暫存的OngoingStubbing
對象返回,這樣我們就能在這上面打樁(調(diào)用thenReturn等方法)睡陪,返回我們需要的值了置逻。打樁完畢會生成一個Answer
對象绑雄,存放到一個鏈表里运准。后面調(diào)用對應的方法的時候辕录,就會從這個鏈表內(nèi)找到對應的Answer
對象诺苹,從中獲取對應的值返回柜砾。
2.5 驗證
方法的執(zhí)行都被我們攔截了驾中,要驗證方法的執(zhí)行也就不是什么難事了疼电。但還是過一下玲躯。
回憶下据德,驗證的代碼verify(mockList, times(2)).get(anyInt())
。為了達成這樣的效果跷车,實現(xiàn)里必須:
- 在verify方法的執(zhí)行過程里棘利,記錄下要驗證的對象,以及要驗證的參數(shù)
- 在執(zhí)行方法調(diào)用的時候朽缴,取出要驗證的對象善玫、驗證的參數(shù),執(zhí)行驗證密强。
當了解了Mockito的設計之后茅郎,這一切都順理成章。這里就不詳細說了或渤,如果大家有興趣系冗,可以去看下Mockito的源碼。
Mockito這個庫的設計思路很特別薪鹦,它的功能的實現(xiàn)并不是在一個執(zhí)行過程里干完掌敬,而是分階段分步驟的執(zhí)行惯豆。但Mockito又很好的保證了這些在不同時空里執(zhí)行的步驟能夠準確的結合起來,共同完成這一個過程涝开。更重要的是循帐,在這種情況下,它所暴露出來的API依舊簡潔優(yōu)雅舀武,對使用者來說幾乎是無感的拄养。
三、單元測試
再好的工具银舱,如果沒有使用起來瘪匿,也只是一個擺設。那么介紹完了Mockito寻馏,接下來我們回過頭來聊聊單元測試棋弥。
首先是幾個概念:
3.1 Mock
Mock一詞指效仿、模仿诚欠,在單元測試里顽染,使用mock來構造一個“替身”。這個替身主要用于作為被測類的依賴關系的替代轰绵。
依賴關系 – 依賴關系是指在應用程序中一個類基于另一個類來執(zhí)行其預定的功能.依賴關系通常都存在于所依賴的類的實例變量中.
被測類 – 在編寫單元測試的時候, “單元”一詞通常代表一個單獨的類及為其編寫的測試代碼. 被測類指的就是其中被測試的類.
為什么需要mock呢粉寞?
真實對象具有不可確定的行為,產(chǎn)生不可預測的效果左腔,(如:股票行情唧垦,天氣預報
真實對象很難被創(chuàng)建的
真實對象的某些行為很難被觸發(fā)
真實對象實際上還不存在的(和其他開發(fā)小組或者和新的硬件打交道)等等
在這些情形下,使用Mock能大大簡化我們的測試難度液样。舉個例子:
假定我們有如上的關系圖:
類A依賴于類B和類C
類B又依賴于類D和類E
為了測試A振亮,我們需要整個依賴樹都構造出來,這未免太麻煩
使用Mock鞭莽,就能將結構分解坊秸,像這樣。從圖中可以清晰的看出澎怒,我們的依賴樹被大大的簡化了褒搔。Mock對象就是在測試的過程中,用來作為真實對象的替代品丹拯。使用了Mock技術的測試站超,也就能稱為Mock測試了。
3.2 Stub
Stub就是打樁乖酬。
Stubbing就是告訴模擬對象當與之交互時執(zhí)行何種行為過程死相。通常它可以用來提供那些測試所需的公共屬性(像getters和setters)和公共方法。
使用Stub咬像,可以根據(jù)我們的需要返回一個特殊的值算撮、拋出一個錯誤生宛、觸發(fā)一個事件,或者肮柜,自定義方法在不同參數(shù)下的不同行為陷舅。
而這并不會增大我們的工作量,相反审洞,減少了我們的工作量莱睁。使用Stub甚至能讓我們在實現(xiàn)被模擬的對象的方法之前去測試我們的代碼。
Stub進一步增強了Mock對象的能力芒澜。Mock本質(zhì)上是對依賴的模擬仰剿,它使得我們擁有了一個依賴。但在測試中痴晦,除了依賴南吮,我們還需要對這個依賴的行為進行控制,這就是Stub要做的事情誊酌。
Stub讓我們能對依賴的行為進行模擬部凑,省略具體的實現(xiàn)邏輯,直接控制行為的結果碧浊,一般用來提供測試時所需的測試數(shù)據(jù)涂邀,驗證交互是否符合預期。
3.3 使用Mock和Stub的好處
- 提前創(chuàng)建測試辉词,比如進行TDD
- 團隊可以并行工作
- 創(chuàng)建演示demo
- 為無法/難以獲取的資源編寫測試
- 隔離系統(tǒng)
- 作為模擬數(shù)據(jù)交付給用戶(假數(shù)據(jù))
3.4 測試流程
進行單元測試時必孤,我們只需關心三樣東西: 設置測試數(shù)據(jù)猾骡,設定預期結果瑞躺,驗證結果。并不是所有的測試都包含著三樣兴想,有的只涉及設置測試數(shù)據(jù)幢哨,有的只涉及設定預期結果和驗證.
模擬替換外部依賴、執(zhí)行測試代碼嫂便、驗證執(zhí)行結果是否符合預期捞镰。簡稱3A原則:Arrange、Act毙替、Assert
3.5 單元測試不是集成測試
剛接觸單元測試的時候岸售,一直很迷惑,我的業(yè)務邏輯那么多那么復雜厂画,這要怎么做單元測試呢凸丸?比如說一個登陸功能,雖然它僅僅是一個登陸功能袱院,但它背后要干的事情可不少:驗證用戶名屎慢,驗證密碼瞭稼,判斷網(wǎng)絡,發(fā)起網(wǎng)絡請求腻惠,等待請求結果环肘,根據(jù)結果執(zhí)行不同的邏輯。
想想都頭大集灌,這樣的單元測試要怎么寫悔雹?
答:這樣的單元測試不用寫。
我們給這個東西做測試的時候欣喧,不是測整個登陸流程荠商。這種測試在測試領域里稱為集成測試,而不是單元測試续誉。集成測試并不是我們(程序員)花精力的地方莱没,而的是測試同事的業(yè)務范圍。
關于測試酷鸦,有一個Test Pyramid理論饰躲,叫測試的金字塔模型。
Test Pyramid理論基本大意是臼隔,單元測試是基礎嘹裂,是我們應該花絕大多數(shù)時間去寫的部分,而集成測試等應該是冰山上面能看見的那一小部分摔握。
為什么是這樣呢寄狼?因為集成測試設置起來很麻煩,運行起來很慢氨淌,發(fā)現(xiàn)的bug少泊愧,在保證代碼質(zhì)量、改善代碼設計方面更起不到任何作用盛正,因此它的重要程度并不是那么高删咱,也無法將它納入我們正常的工作流程中。
而單元測試則剛好相反豪筝,它運行速度超快痰滋,能發(fā)現(xiàn)的bug更多,在開發(fā)時能引導更好的代碼設計续崖,在重構時能保證重構的正確性敲街,因此它能保證我們的代碼在一個比較高的質(zhì)量水平上。同時因為運行速度快严望,我們很容易把它納入到我們正常的開發(fā)流程中多艇。
至于為什么集成測試發(fā)現(xiàn)的bug少,而單元測試發(fā)現(xiàn)的bug多著蟹,這里也稍作解釋墩蔓,因為集成測試不能測試到其中每個環(huán)節(jié)的每個方面梢莽,某一個集成測試運行正確了,不代表另一個集成測試也能運行正確奸披。而單元測試會比較完整的測試每個單元的各種不同的狀況昏名、臨界條件等等。一般來說阵面,如果每一個環(huán)節(jié)是對的轻局,那么在很大的概率上,整個流程就是對的样刷。雖然不能保證整個流程100%一定是對的仑扑。所以,集成測試需要有置鼻,但應該是少量镇饮,單元測試是我們應該花重點去做的事情。
3.6 為什么要進行單元測試
常見的理由有:
- 對軟件質(zhì)量的提升
- 方便重構
- 節(jié)約時間
- 提升代碼設計
- ...
但以上的理由卻很難得到證明箕母。軟件質(zhì)量的提升储藐,如何通過數(shù)據(jù)來表明?方便重構嘶是,這個必要性很大嗎钙勃?尤其是在工期緊張,功能優(yōu)先的情況下聂喇。需求都做不完辖源,哪有時間寫測試,更何談節(jié)約時間希太。至于代碼設計提升克饶,更多的不是工程師的素養(yǎng)問題嗎。
那么單元測試有沒有別的作用跛十?
當我們參與到新項目彤路,接手維護舊模塊秕硝,其實挺讓人驚恐的芥映。對項目結構的不熟悉、各模塊各部分之間的關聯(lián)也難以理清远豺,有些還不一定能理清奈偏。經(jīng)常改動一個地方,結果莫名其妙的引起了別的地方的問題躯护,如果改動的是框架層上的東西惊来,那更讓人蛋疼了。業(yè)務用法千千萬棺滞,一個一個手動測試裁蚁,哪里來得及矢渊,就算來得及,重復幾遍也讓人蛋疼枉证。
對于用戶量大的應用矮男,如QQ音樂、全民K歌室谚,一天幾千萬的DAU毡鉴,出一個bug,crash率上漲秒赤、外網(wǎng)投訴量蹭蹭蹭的漲猪瞬,遇上這種時候肯定是內(nèi)心十萬個草泥馬...要是遇上一個特殊的場景,非必現(xiàn)入篮,用戶復現(xiàn)路徑復雜陈瘦,定位調(diào)試也要耗費大量時間。
這種情況下潮售,單元測試才是一枚更好的解藥甘晤。單元測試僅是對一個代碼單元進行測試,保證一個代碼單元的正確可比保證整個APP的準確容易饲做,遍歷這個代碼單元的所有參數(shù)輸入和輸出线婚,也比驗證所有的用戶場景容易,重點是盆均,跑一次單元測試塞弊,比運行一次手動測試快!而且還可以交給程序自動化泪姨。人的天性總是懶惰的游沿。
另外一個,如果代碼中有一些陳年代碼肮砾,如果想要對其進行重構诀黍,如果沒有單元測試,想要動手去重構想必也是需要一定勇氣仗处。而單元測試眯勾,可以成為我們的一道保障,讓我們在改動代碼的時候不需要顧慮太多婆誓,正確性由單元測試來驗證和保障吃环。這也是<重構>一書里不斷強調(diào)的。
節(jié)省時間:
上面提到了Mock可以用來協(xié)同工作洋幻。這里舉個例子:
我們做需求的時候郁轻,對于有一定經(jīng)驗,有一定代碼思想的人來說,當他拿到一個新的需求好唯,他會先想想代碼的結構竭沫,應該有那些類,那些組件骑篙,什么責任應該劃分到哪里去输吏,然后才開始動手寫代碼,這個是很自然的一個思維過程替蛉。
但這樣一來贯溅,我們要驗證我們的代碼正確性的時候,就只能等到每個部分都搞定在驗證了躲查?這顯然是低效的它浅,有的部分還涉及了前后臺聯(lián)動等外部條件的制約,每個部分都搞定了也不一定能測試镣煮。而且姐霍,每個未經(jīng)測試的代碼整合在一起,出錯的時候往往還要花上相當?shù)臅r間卻定位問題出在哪部分上典唇,然后修改镊折,部署/安裝,重復驗證介衔。
如果有單元測試恨胚,結合Mock,我們就能在編寫每個小功能塊的同時炎咖,對其進行驗證赃泡。
使用單元測試,能夠給我們:
- 更快的結果反饋
- 帶來更少的bug(開發(fā)自測)乘盼,也更容易發(fā)現(xiàn)bug(回歸測試)
- 節(jié)約時間(不在受限于外部條件的制約無法驗證)
- 更好的設計(為了寫出便于測試的代碼升熊,會開始思考程序的架構是否合理,保持單一責任绸栅,減低耦合级野,傾向于組合,而不是繼承)
3.7 如何開展單元測試
- 從現(xiàn)在開始粹胯,一點一點的寫蓖柔,有總好過沒有
- 在測試過程中,逐漸建立自己的工具箱矛双,相似的場合測試大同小異渊抽,抽公共部分作為輔助類,便于測試
- 如果當前項目里沒有單元測試议忽,引入起來有點困難,那么先在新的代碼里引入十减,后面慢慢調(diào)整項目結構栈幸,將測試覆蓋開去
四愤估、參考資料
反模式的經(jīng)典 - Mockito設計解析
JUnit + Mockito 單元測試(二)
Android單元測試(四):Mock以及Mockito的使用
5分鐘了解Mockito
Mockito 簡明教程
Mockito源碼解析
[譯] 使用強大的 Mockito 測試框架來測試你的代碼
Mockito:一個強大的用于 Java 開發(fā)的模擬測試框架
Android單元測試: 首先,從是什么開始
Android單元測試(二):再來談談為什么