一、百變怪 Mockito
Mockito可謂是Java世界的百變怪,使用它,可以輕易的復(fù)制出各種類型的對(duì)象,并與之進(jìn)行交互。
1.1 對(duì)象“復(fù)制”
// 列表
List mockList = mock(List.class);
mockList.add(1);
mockList.clear();
// Socket對(duì)象
Socket mockSocket = mock(Socket);
mockSocket.connect(new InetSocketAddress(8080));
mockSocket.close();
1.2 技能復(fù)制
List mockList = mock(List.class);
mockList.add(1); // 簡(jiǎn)單交互
mockList.get(1); // 返回值為null
mockList.size(); // 返回值為0
雖然復(fù)制出來(lái)的對(duì)象上的所有方法都能被調(diào)用,但好像這個(gè)百變怪的技能有點(diǎn)弱呢...
其實(shí),是使用方式不對(duì),這個(gè)百變怪掌握的僅僅是基礎(chǔ)技能,對(duì)于有返回值的調(diào)用,只會(huì)返回默認(rèn)的返回值,在需要返回對(duì)象的場(chǎng)合,返回null
,需要返回int
的場(chǎng)合,返回0
。其他的默認(rèn)返回值,見(jiàn)下表:
// todo 默認(rèn)返回值表
要讓它能按我們的需要展現(xiàn)技能(方法),需要事先“教會(huì)”它。
List mockList = mock(List.class);
when(mockList.get(anyInt()).thenReturn(1);
when(mockList.size()).thenReturn(1, 2, 3);
assertEquals("預(yù)期返回1", 1, mockList.get(1)); // pass
assertEquals("預(yù)期返回1", 1, mockList.get(2)); // pass
assertEquals("預(yù)期返回1", 1, mockList.get(3)); // pass
assertEquals("預(yù)期返回1", 1, mockList.size()); // pass
assertEquals("預(yù)期返回2", 2, mockList.size()); // pass
assertEquals("預(yù)期返回3", 3, mockList.size()); // pass
上面的代碼,我們教會(huì)了這個(gè)百變怪:
- 只要調(diào)用
get
方法,不管參數(shù)是什么,都返回1
; - 對(duì)于
size
方法調(diào)用,第一次返回1
,第二次調(diào)用返回2
,第三次開(kāi)始,則返回3
。
是的,這個(gè)百變怪就是這么的笨,只會(huì)有樣學(xué)樣。看起來(lái)一點(diǎn)用都沒(méi)有。
1.3 驗(yàn)證
但是呢,雖然它笨,但是它卻具備一些“笨方法”,也不算沒(méi)有用。
verify(mockList, never()).clear(); // 從未調(diào)用過(guò)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(); // 這里會(huì)失敗,因?yàn)樯厦嫖覀冎徽{(diào)用了size方法3次
可以看到,這個(gè)百變怪雖然笨,但不傻,對(duì)于它自己做過(guò)了什么,它是記得一清二楚的。至于它還有什么其他技能,可以到官網(wǎng)看下他的使用說(shuō)明書(shū)詳細(xì)了解。
1.4 小結(jié)
可以看到,雖然Mockito在正式的場(chǎng)合(生產(chǎn)環(huán)境)下派不上什么用場(chǎng),但在訓(xùn)練場(chǎng)(測(cè)試環(huán)境)上,卻能夠成為一個(gè)相當(dāng)不錯(cuò)的陪練。
所以,Mockito是一個(gè)適用于單元測(cè)試的mock庫(kù)。在單元測(cè)試中,可以通過(guò)它來(lái)方便的生成模擬對(duì)象。便于進(jìn)行測(cè)試。
二、Mockito與單元測(cè)試
2.1 例
假設(shè)我們有一段業(yè)務(wù)邏輯,需要對(duì)給定的請(qǐng)求做處理,在這種情況下,倘若要手工構(gòu)造發(fā)起一個(gè)請(qǐng)求,那想必是很麻煩蛋疼。首先我們需要把代碼編譯部署到測(cè)試服務(wù)器上,然后構(gòu)造并發(fā)起一個(gè)請(qǐng)求,等待服務(wù)器接收到請(qǐng)求后,交給我們的業(yè)務(wù)進(jìn)行處理。如下:
// 業(yè)務(wù)代碼
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;
}
為了測(cè)試這么一點(diǎn)點(diǎn)代碼,就需要我們額外付出那么多的操作,對(duì)于追求效率的程序員來(lái)說(shuō),這種重復(fù)操作&等待簡(jiǎn)直就是慢性自殺。這里的代碼還是相對(duì)簡(jiǎn)單的,要是請(qǐng)求的內(nèi)容更加復(fù)雜,難道還要花上大把時(shí)間研究如何構(gòu)造出這么一個(gè)Http請(qǐng)求嗎?
其實(shí),測(cè)試這段邏輯,我們想要做的事情其實(shí)很簡(jiǎn)單,給定一個(gè)特定的輸入,驗(yàn)證其輸出結(jié)果是否正確。也就是,驗(yàn)證的過(guò)程,應(yīng)該盡可能的簡(jiǎn)單方便,把大部分的時(shí)間耗費(fèi)在驗(yàn)證過(guò)程上絕對(duì)是有問(wèn)題的。
如果我們使用單元測(cè)試,搭配Mockito,完全可以寫(xiě)出如下測(cè)試,在代碼提交之前,先在本地的JVM上過(guò)一遍測(cè)試。
@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)
}
首先,我們模擬出一個(gè)假對(duì)象,并設(shè)定這個(gè)假對(duì)象的行為,這個(gè)假對(duì)象的行為會(huì)影響我們業(yè)務(wù)邏輯的結(jié)果,所以我們可以在不同的測(cè)試用例里,設(shè)定假對(duì)象返回不同的行為,這樣我們就能驗(yàn)證各種輸入下,我們的業(yè)務(wù)邏輯是不是能夠按我們的設(shè)想正常工作。
2.2 Mockito 原理剖析
Ok,到現(xiàn)在為止,我們通過(guò)幾個(gè)例子簡(jiǎn)單的展示了Mockito,以及它在單元測(cè)試中起到作用。從例子中可以看到,Mockito的使用是很直觀的,使用起來(lái)行云流水,就跟說(shuō)話一樣自然。某種程度上,也可以看做代碼即注釋
的一種表現(xiàn)。當(dāng)然這有點(diǎn)扯遠(yuǎn)了。
Mockito的這種神乎其技的使用方式,使得我在一開(kāi)始見(jiàn)到它的時(shí)候,感到驚訝,驚訝之余又感到不解。
如mock(List.class)
,怎么就能夠從List.class
這個(gè)接口搞出一個(gè)可以用的對(duì)象?when(mockList.size()).thenReturn(20)
這種,竟然就能干預(yù)到mock對(duì)象的執(zhí)行,插樁返回了20。mockList.size()
本身不就是一個(gè)方法調(diào)用嗎?verify(mockList, never()).add(10)
,這種驗(yàn)證方式又是通過(guò)什么黑科技實(shí)現(xiàn)的???
看著Mockito
的使用文檔的我,當(dāng)時(shí)真是一臉黑人問(wèn)號(hào)。
后來(lái),從我有限的知識(shí)儲(chǔ)備里,我想到了mock
的實(shí)現(xiàn)方式可能是使用泛型 + 動(dòng)態(tài)代理
實(shí)現(xiàn),當(dāng)想到這種組合的時(shí)候,我不禁感慨庫(kù)作者的思維的精妙,所以我決定研究下Mockito的源碼,看看作者是怎么做到的。
當(dāng)然,后來(lái)我發(fā)現(xiàn),泛型是用到了(廢話),動(dòng)態(tài)代理技術(shù)卻沒(méi)有用到。好了,閑話不多說(shuō),下面來(lái)講講Mockito
的實(shí)現(xiàn)。由于在座同學(xué),平時(shí)使用Java應(yīng)該不多,所以這里我就不深入講解細(xì)節(jié),會(huì)比較偏向原理性的東西。
2.3 Mock
讓我們來(lái)分析一下,要mock一個(gè)對(duì)象,我們需要做什么。
- 首先需要知道要Mock的對(duì)象的類型,這樣我們才能生成這個(gè)類型的對(duì)象
- 為了生成這個(gè)類型的對(duì)象,那么這個(gè)類型需要是能實(shí)例化的,但如果這個(gè)類型是抽象類或者一個(gè)接口?要怎么辦?我們知道,抽象類和接口需要被實(shí)現(xiàn),才能實(shí)例化,因此,最自然的方式就是,繼承自這個(gè)類型,然后給這些方法一個(gè)空實(shí)現(xiàn)。
- 有了可以實(shí)例化的類型,接下來(lái)就好辦了:實(shí)例化這個(gè)類型,并上轉(zhuǎn)型成我們的目標(biāo)類,返回。
總結(jié)起來(lái)就是:給到要mock的類型、生成一個(gè)繼承這個(gè)類型的類、實(shí)例化生成的類、得到mock對(duì)象。
Mockito的源碼里正是這么做的:
- 暴露出Mockito.mock接口給使用者
- 得到要mock的類型,進(jìn)行一些設(shè)置,然后一路傳遞到
SubclassBytecodeGenerator
,由它來(lái)生成mock類型的子類 - 得到這個(gè)類型后,
SubclassByteBuddyMockMaker
將其實(shí)例化
第二步的實(shí)現(xiàn)借助了ByteBuddy
這個(gè)框架,這個(gè)框架可以直接生成Java的類,然后通過(guò)ClassLoader加載進(jìn)來(lái)使用。這里就不深入了。
第三步實(shí)例化,實(shí)例化使用了objenesis
,一個(gè)能在不同平臺(tái)上實(shí)例化一個(gè)類的庫(kù)。
經(jīng)過(guò)這幾步,就得到了一個(gè)可以用來(lái)操作的模擬對(duì)象。
實(shí)現(xiàn)的思路大致是這樣,代碼里的處理還有很多細(xì)節(jié)性的部分,這里不進(jìn)行源碼探究,就不多講了
2.4 打樁
when這一步要實(shí)現(xiàn)的功能是打樁。
那么,對(duì)于when(mockType.someMethod()).thenReturn(value)
這樣的方法調(diào)用,該怎么實(shí)現(xiàn)?
一開(kāi)始我以為方法調(diào)用的返回值有貓膩,返回值唯一標(biāo)識(shí)一次方法調(diào)用,通過(guò)在內(nèi)部記錄這個(gè)值,來(lái)返回特定的值。但對(duì)于每個(gè)方法調(diào)用,返回一個(gè)特定的返回值并不可能,何況有的方法調(diào)用并沒(méi)有返回值。
這個(gè)功能Mockito是這么實(shí)現(xiàn)的:
在mock那一步,我們知道了Mockito生成了一個(gè)派生類,派生類里的所有方法調(diào)用,也已經(jīng)被hook掉,即所有的方法調(diào)用,并不會(huì)執(zhí)行到原有的實(shí)現(xiàn)邏輯里,而是會(huì)返回一個(gè)默認(rèn)值。
所有的方法調(diào)用最終都會(huì)交由MockHandlerImpl.handle
來(lái)執(zhí)行。這個(gè)類很重要,可以說(shuō)是Mockito整個(gè)功能的核心所在。
在進(jìn)行方法調(diào)用的時(shí)候,Mockito會(huì)假定這個(gè)方法調(diào)用需要被打樁,生成一個(gè)和這個(gè)方法調(diào)用相對(duì)應(yīng)的OngoingStubbing
對(duì)象,將這個(gè)對(duì)象暫時(shí)存起來(lái)。
當(dāng)when
方法執(zhí)行的時(shí)候,就會(huì)取出這個(gè)暫存的OngoingStubbing
對(duì)象返回,這樣我們就能在這上面打樁(調(diào)用thenReturn等方法),返回我們需要的值了。打樁完畢會(huì)生成一個(gè)Answer
對(duì)象,存放到一個(gè)鏈表里。后面調(diào)用對(duì)應(yīng)的方法的時(shí)候,就會(huì)從這個(gè)鏈表內(nèi)找到對(duì)應(yīng)的Answer
對(duì)象,從中獲取對(duì)應(yīng)的值返回。
2.5 驗(yàn)證
方法的執(zhí)行都被我們攔截了,要驗(yàn)證方法的執(zhí)行也就不是什么難事了。但還是過(guò)一下。
回憶下,驗(yàn)證的代碼verify(mockList, times(2)).get(anyInt())
。為了達(dá)成這樣的效果,實(shí)現(xiàn)里必須:
- 在verify方法的執(zhí)行過(guò)程里,記錄下要驗(yàn)證的對(duì)象,以及要驗(yàn)證的參數(shù)
- 在執(zhí)行方法調(diào)用的時(shí)候,取出要驗(yàn)證的對(duì)象、驗(yàn)證的參數(shù),執(zhí)行驗(yàn)證。
當(dāng)了解了Mockito的設(shè)計(jì)之后,這一切都順理成章。這里就不詳細(xì)說(shuō)了,如果大家有興趣,可以去看下Mockito的源碼。
Mockito這個(gè)庫(kù)的設(shè)計(jì)思路很特別,它的功能的實(shí)現(xiàn)并不是在一個(gè)執(zhí)行過(guò)程里干完,而是分階段分步驟的執(zhí)行。但Mockito又很好的保證了這些在不同時(shí)空里執(zhí)行的步驟能夠準(zhǔn)確的結(jié)合起來(lái),共同完成這一個(gè)過(guò)程。更重要的是,在這種情況下,它所暴露出來(lái)的API依舊簡(jiǎn)潔優(yōu)雅,對(duì)使用者來(lái)說(shuō)幾乎是無(wú)感的。
三、單元測(cè)試
再好的工具,如果沒(méi)有使用起來(lái),也只是一個(gè)擺設(shè)。那么介紹完了Mockito,接下來(lái)我們回過(guò)頭來(lái)聊聊單元測(cè)試。
首先是幾個(gè)概念:
3.1 Mock
Mock一詞指效仿、模仿,在單元測(cè)試?yán)铮褂胢ock來(lái)構(gòu)造一個(gè)“替身”。這個(gè)替身主要用于作為被測(cè)類的依賴關(guān)系的替代。
依賴關(guān)系 – 依賴關(guān)系是指在應(yīng)用程序中一個(gè)類基于另一個(gè)類來(lái)執(zhí)行其預(yù)定的功能.依賴關(guān)系通常都存在于所依賴的類的實(shí)例變量中.
被測(cè)類 – 在編寫(xiě)單元測(cè)試的時(shí)候, “單元”一詞通常代表一個(gè)單獨(dú)的類及為其編寫(xiě)的測(cè)試代碼. 被測(cè)類指的就是其中被測(cè)試的類.
為什么需要mock呢?
真實(shí)對(duì)象具有不可確定的行為,產(chǎn)生不可預(yù)測(cè)的效果,(如:股票行情,天氣預(yù)報(bào)
真實(shí)對(duì)象很難被創(chuàng)建的
真實(shí)對(duì)象的某些行為很難被觸發(fā)
真實(shí)對(duì)象實(shí)際上還不存在的(和其他開(kāi)發(fā)小組或者和新的硬件打交道)等等
在這些情形下,使用Mock能大大簡(jiǎn)化我們的測(cè)試難度。舉個(gè)例子:
假定我們有如上的關(guān)系圖:
類A依賴于類B和類C
類B又依賴于類D和類E
為了測(cè)試A,我們需要整個(gè)依賴樹(shù)都構(gòu)造出來(lái),這未免太麻煩
使用Mock,就能將結(jié)構(gòu)分解,像這樣。從圖中可以清晰的看出,我們的依賴樹(shù)被大大的簡(jiǎn)化了。Mock對(duì)象就是在測(cè)試的過(guò)程中,用來(lái)作為真實(shí)對(duì)象的替代品。使用了Mock技術(shù)的測(cè)試,也就能稱為Mock測(cè)試了。
3.2 Stub
Stub就是打樁。
Stubbing就是告訴模擬對(duì)象當(dāng)與之交互時(shí)執(zhí)行何種行為過(guò)程。通常它可以用來(lái)提供那些測(cè)試所需的公共屬性(像getters和setters)和公共方法。
使用Stub,可以根據(jù)我們的需要返回一個(gè)特殊的值、拋出一個(gè)錯(cuò)誤、觸發(fā)一個(gè)事件,或者,自定義方法在不同參數(shù)下的不同行為。
而這并不會(huì)增大我們的工作量,相反,減少了我們的工作量。使用Stub甚至能讓我們?cè)趯?shí)現(xiàn)被模擬的對(duì)象的方法之前去測(cè)試我們的代碼。
Stub進(jìn)一步增強(qiáng)了Mock對(duì)象的能力。Mock本質(zhì)上是對(duì)依賴的模擬,它使得我們擁有了一個(gè)依賴。但在測(cè)試中,除了依賴,我們還需要對(duì)這個(gè)依賴的行為進(jìn)行控制,這就是Stub要做的事情。
Stub讓我們能對(duì)依賴的行為進(jìn)行模擬,省略具體的實(shí)現(xiàn)邏輯,直接控制行為的結(jié)果,一般用來(lái)提供測(cè)試時(shí)所需的測(cè)試數(shù)據(jù),驗(yàn)證交互是否符合預(yù)期。
3.3 使用Mock和Stub的好處
- 提前創(chuàng)建測(cè)試,比如進(jìn)行TDD
- 團(tuán)隊(duì)可以并行工作
- 創(chuàng)建演示demo
- 為無(wú)法/難以獲取的資源編寫(xiě)測(cè)試
- 隔離系統(tǒng)
- 作為模擬數(shù)據(jù)交付給用戶(假數(shù)據(jù))
3.4 測(cè)試流程
進(jìn)行單元測(cè)試時(shí),我們只需關(guān)心三樣?xùn)|西: 設(shè)置測(cè)試數(shù)據(jù),設(shè)定預(yù)期結(jié)果,驗(yàn)證結(jié)果。并不是所有的測(cè)試都包含著三樣,有的只涉及設(shè)置測(cè)試數(shù)據(jù),有的只涉及設(shè)定預(yù)期結(jié)果和驗(yàn)證.
模擬替換外部依賴、執(zhí)行測(cè)試代碼、驗(yàn)證執(zhí)行結(jié)果是否符合預(yù)期。簡(jiǎn)稱3A原則:Arrange、Act、Assert
3.5 單元測(cè)試不是集成測(cè)試
剛接觸單元測(cè)試的時(shí)候,一直很迷惑,我的業(yè)務(wù)邏輯那么多那么復(fù)雜,這要怎么做單元測(cè)試呢?比如說(shuō)一個(gè)登陸功能,雖然它僅僅是一個(gè)登陸功能,但它背后要干的事情可不少:驗(yàn)證用戶名,驗(yàn)證密碼,判斷網(wǎng)絡(luò),發(fā)起網(wǎng)絡(luò)請(qǐng)求,等待請(qǐng)求結(jié)果,根據(jù)結(jié)果執(zhí)行不同的邏輯。
想想都頭大,這樣的單元測(cè)試要怎么寫(xiě)?
答:這樣的單元測(cè)試不用寫(xiě)。
我們給這個(gè)東西做測(cè)試的時(shí)候,不是測(cè)整個(gè)登陸流程。這種測(cè)試在測(cè)試領(lǐng)域里稱為集成測(cè)試,而不是單元測(cè)試。集成測(cè)試并不是我們(程序員)花精力的地方,而的是測(cè)試同事的業(yè)務(wù)范圍。
關(guān)于測(cè)試,有一個(gè)Test Pyramid理論,叫測(cè)試的金字塔模型。
Test Pyramid理論基本大意是,單元測(cè)試是基礎(chǔ),是我們應(yīng)該花絕大多數(shù)時(shí)間去寫(xiě)的部分,而集成測(cè)試等應(yīng)該是冰山上面能看見(jiàn)的那一小部分。
為什么是這樣呢?因?yàn)榧蓽y(cè)試設(shè)置起來(lái)很麻煩,運(yùn)行起來(lái)很慢,發(fā)現(xiàn)的bug少,在保證代碼質(zhì)量、改善代碼設(shè)計(jì)方面更起不到任何作用,因此它的重要程度并不是那么高,也無(wú)法將它納入我們正常的工作流程中。
而單元測(cè)試則剛好相反,它運(yùn)行速度超快,能發(fā)現(xiàn)的bug更多,在開(kāi)發(fā)時(shí)能引導(dǎo)更好的代碼設(shè)計(jì),在重構(gòu)時(shí)能保證重構(gòu)的正確性,因此它能保證我們的代碼在一個(gè)比較高的質(zhì)量水平上。同時(shí)因?yàn)檫\(yùn)行速度快,我們很容易把它納入到我們正常的開(kāi)發(fā)流程中。
至于為什么集成測(cè)試發(fā)現(xiàn)的bug少,而單元測(cè)試發(fā)現(xiàn)的bug多,這里也稍作解釋,因?yàn)榧蓽y(cè)試不能測(cè)試到其中每個(gè)環(huán)節(jié)的每個(gè)方面,某一個(gè)集成測(cè)試運(yùn)行正確了,不代表另一個(gè)集成測(cè)試也能運(yùn)行正確。而單元測(cè)試會(huì)比較完整的測(cè)試每個(gè)單元的各種不同的狀況、臨界條件等等。一般來(lái)說(shuō),如果每一個(gè)環(huán)節(jié)是對(duì)的,那么在很大的概率上,整個(gè)流程就是對(duì)的。雖然不能保證整個(gè)流程100%一定是對(duì)的。所以,集成測(cè)試需要有,但應(yīng)該是少量,單元測(cè)試是我們應(yīng)該花重點(diǎn)去做的事情。
3.6 為什么要進(jìn)行單元測(cè)試
常見(jiàn)的理由有:
- 對(duì)軟件質(zhì)量的提升
- 方便重構(gòu)
- 節(jié)約時(shí)間
- 提升代碼設(shè)計(jì)
- ...
但以上的理由卻很難得到證明。軟件質(zhì)量的提升,如何通過(guò)數(shù)據(jù)來(lái)表明?方便重構(gòu),這個(gè)必要性很大嗎?尤其是在工期緊張,功能優(yōu)先的情況下。需求都做不完,哪有時(shí)間寫(xiě)測(cè)試,更何談節(jié)約時(shí)間。至于代碼設(shè)計(jì)提升,更多的不是工程師的素養(yǎng)問(wèn)題嗎。
那么單元測(cè)試有沒(méi)有別的作用?
當(dāng)我們參與到新項(xiàng)目,接手維護(hù)舊模塊,其實(shí)挺讓人驚恐的。對(duì)項(xiàng)目結(jié)構(gòu)的不熟悉、各模塊各部分之間的關(guān)聯(lián)也難以理清,有些還不一定能理清。經(jīng)常改動(dòng)一個(gè)地方,結(jié)果莫名其妙的引起了別的地方的問(wèn)題,如果改動(dòng)的是框架層上的東西,那更讓人蛋疼了。業(yè)務(wù)用法千千萬(wàn),一個(gè)一個(gè)手動(dòng)測(cè)試,哪里來(lái)得及,就算來(lái)得及,重復(fù)幾遍也讓人蛋疼。
對(duì)于用戶量大的應(yīng)用,如QQ音樂(lè)、全民K歌,一天幾千萬(wàn)的DAU,出一個(gè)bug,crash率上漲、外網(wǎng)投訴量蹭蹭蹭的漲,遇上這種時(shí)候肯定是內(nèi)心十萬(wàn)個(gè)草泥馬...要是遇上一個(gè)特殊的場(chǎng)景,非必現(xiàn),用戶復(fù)現(xiàn)路徑復(fù)雜,定位調(diào)試也要耗費(fèi)大量時(shí)間。
這種情況下,單元測(cè)試才是一枚更好的解藥。單元測(cè)試僅是對(duì)一個(gè)代碼單元進(jìn)行測(cè)試,保證一個(gè)代碼單元的正確可比保證整個(gè)APP的準(zhǔn)確容易,遍歷這個(gè)代碼單元的所有參數(shù)輸入和輸出,也比驗(yàn)證所有的用戶場(chǎng)景容易,重點(diǎn)是,跑一次單元測(cè)試,比運(yùn)行一次手動(dòng)測(cè)試快!而且還可以交給程序自動(dòng)化。人的天性總是懶惰的。
另外一個(gè),如果代碼中有一些陳年代碼,如果想要對(duì)其進(jìn)行重構(gòu),如果沒(méi)有單元測(cè)試,想要?jiǎng)邮秩ブ貥?gòu)想必也是需要一定勇氣。而單元測(cè)試,可以成為我們的一道保障,讓我們?cè)诟膭?dòng)代碼的時(shí)候不需要顧慮太多,正確性由單元測(cè)試來(lái)驗(yàn)證和保障。這也是<重構(gòu)>一書(shū)里不斷強(qiáng)調(diào)的。
節(jié)省時(shí)間:
上面提到了Mock可以用來(lái)協(xié)同工作。這里舉個(gè)例子:
我們做需求的時(shí)候,對(duì)于有一定經(jīng)驗(yàn),有一定代碼思想的人來(lái)說(shuō),當(dāng)他拿到一個(gè)新的需求,他會(huì)先想想代碼的結(jié)構(gòu),應(yīng)該有那些類,那些組件,什么責(zé)任應(yīng)該劃分到哪里去,然后才開(kāi)始動(dòng)手寫(xiě)代碼,這個(gè)是很自然的一個(gè)思維過(guò)程。
但這樣一來(lái),我們要驗(yàn)證我們的代碼正確性的時(shí)候,就只能等到每個(gè)部分都搞定在驗(yàn)證了?這顯然是低效的,有的部分還涉及了前后臺(tái)聯(lián)動(dòng)等外部條件的制約,每個(gè)部分都搞定了也不一定能測(cè)試。而且,每個(gè)未經(jīng)測(cè)試的代碼整合在一起,出錯(cuò)的時(shí)候往往還要花上相當(dāng)?shù)臅r(shí)間卻定位問(wèn)題出在哪部分上,然后修改,部署/安裝,重復(fù)驗(yàn)證。
如果有單元測(cè)試,結(jié)合Mock,我們就能在編寫(xiě)每個(gè)小功能塊的同時(shí),對(duì)其進(jìn)行驗(yàn)證。
使用單元測(cè)試,能夠給我們:
- 更快的結(jié)果反饋
- 帶來(lái)更少的bug(開(kāi)發(fā)自測(cè)),也更容易發(fā)現(xiàn)bug(回歸測(cè)試)
- 節(jié)約時(shí)間(不在受限于外部條件的制約無(wú)法驗(yàn)證)
- 更好的設(shè)計(jì)(為了寫(xiě)出便于測(cè)試的代碼,會(huì)開(kāi)始思考程序的架構(gòu)是否合理,保持單一責(zé)任,減低耦合,傾向于組合,而不是繼承)
3.7 如何開(kāi)展單元測(cè)試
- 從現(xiàn)在開(kāi)始,一點(diǎn)一點(diǎn)的寫(xiě),有總好過(guò)沒(méi)有
- 在測(cè)試過(guò)程中,逐漸建立自己的工具箱,相似的場(chǎng)合測(cè)試大同小異,抽公共部分作為輔助類,便于測(cè)試
- 如果當(dāng)前項(xiàng)目里沒(méi)有單元測(cè)試,引入起來(lái)有點(diǎn)困難,那么先在新的代碼里引入,后面慢慢調(diào)整項(xiàng)目結(jié)構(gòu),將測(cè)試覆蓋開(kāi)去
四、參考資料
反模式的經(jīng)典 - Mockito設(shè)計(jì)解析
JUnit + Mockito 單元測(cè)試(二)
Android單元測(cè)試(四):Mock以及Mockito的使用
5分鐘了解Mockito
Mockito 簡(jiǎn)明教程
Mockito源碼解析
[譯] 使用強(qiáng)大的 Mockito 測(cè)試框架來(lái)測(cè)試你的代碼
Mockito:一個(gè)強(qiáng)大的用于 Java 開(kāi)發(fā)的模擬測(cè)試框架
Android單元測(cè)試: 首先,從是什么開(kāi)始
Android單元測(cè)試(二):再來(lái)談?wù)劄槭裁?/a>