引言
這篇文章主要是總結(jié)一下我自己在學(xué)習(xí)Android單元測試過程中的收獲及感悟,同時也希望可以幫助到正在學(xué)習(xí)Android單元測試的小伙伴們.由于時間及經(jīng)驗有限,文中可能存在錯誤與不足,歡迎大家指出,我會在第一時間對文章進(jìn)行修改糾正.
本文主要包含以下內(nèi)容:
- 什么是單元測試
- 為什么需要進(jìn)行單元測試
- 如何進(jìn)行單元測試
什么是單元測試
首先總結(jié)一下什么是單元測試,單元測試中的單元在Android或Java中可以理解為某個類中的某一個方法,因此單元測試就是針對Android或Java中某個類中的某一個方法中的邏輯代碼進(jìn)行驗證即測試該方法是不是可以正常工作。
還有一點就是要區(qū)分單元測試與集成測試(功能測試、UI測試),單元測試是針對單元即方法的測試,被測單元粒度要小并且具備獨立性,而集成測試是測試多個單元(方法)組合成的功能模塊。
為什么需要進(jìn)行單元測試
-
單元測試的測試相對于集成測試的測試成本較低
單元測試相對于集成測試有運行時間短、投入成本低的優(yōu)勢即Test Pyramid理論:
Test Pyramid
從上圖可以看出單元測試,測試速度快投入成本少
因此我們要將大部分精力投放在單元測試中,保證單元測試的質(zhì)量之后再進(jìn)行集成測試與UI測試來提高測試效率 -
提高開發(fā)效率
開發(fā)Android App的小伙伴可能都會有這樣一個體會,就是當(dāng)App項目逐漸增大,運行App進(jìn)行調(diào)試會花費大量時間在項目的構(gòu)建、編譯、打包、安裝上。這個過程的持續(xù)時間與App的規(guī)模成線性相關(guān)即App項目規(guī)模越大持續(xù)時間就越久。因此隨著我們的的項目逐漸增大,運行App的進(jìn)行調(diào)試時,我們的調(diào)試成本也在逐漸增加。
而單元測試正好能解決這個問題。
舉個例子:
在登錄Activity中有個checkPhoneNum方法,這個方法的功能是在點擊登錄按鈕時,對用戶輸入的登錄賬號進(jìn)行本地的合法性驗證避免不必要的網(wǎng)絡(luò)請求,如果是通過運行App來驗證checkPhoneNum方法是否能夠正常運行,需要經(jīng)過構(gòu)建、編譯、打包、安裝的過程,程序運行之后還需要人工操作進(jìn)入登陸頁面,輸入賬號密碼,點擊登錄按鈕,觸發(fā)checkPhoneNum方法,這個過程可能需要幾十秒甚至一分多鐘,如果通過MVP架構(gòu)將checkPhoneNum作為純Java代碼抽離出來,屏蔽對Android平臺的依賴,就能將單元測試運行在JVM上,并針對checkPhoneNum方法進(jìn)行測試,免去了構(gòu)建、編譯、打包、安裝的過程,整個驗證過程就在一秒之內(nèi),開發(fā)效率將大幅提升。(大致的測試流程在下個章節(jié)進(jìn)行說明)
public boolean checkPhoneNum(String phoneNum){
//判斷phoneNum是否為空(實際的判斷會稍微復(fù)雜一點,為了舉例做了簡化)
if(phoneNum == null || "".equals(phoneNum)){
return false;
}
return true;
}
-
提升項目工程代碼質(zhì)量
進(jìn)行單元測試前提之一就是被測單元具備可測性,以上面checkPhoneNum方法為例,如果checkPhoneNum方法中的代碼直接寫在登錄按鈕的點擊事件中,而沒有抽取為checkPhoneNum方法,那么對這段代碼進(jìn)行單元測試是會非常困難的,極端情況甚至無法測試。所以為了寫出可測試的代碼可以鍛煉開發(fā)人員對的代碼的抽象能力和加強(qiáng)對項目架構(gòu)的把控,從而提升項目工程代碼質(zhì)量。 -
快速定位Bug
由于單元測試對被測項目中的被測單元的獨立性的要求,因此在被測單元的執(zhí)行結(jié)果與預(yù)期結(jié)果不一致時我們就能快速的定位到出現(xiàn)Bug的方法。(在下個章節(jié)中會舉例說明)
如何進(jìn)行單元測試
在Android中進(jìn)行單元測試有很多方案,主要可以分為兩類
在運行在JVM上,不依賴Android環(huán)境
如基礎(chǔ)的 JUnit+Mockito+MVP 或比較全面的JUnit + Mockito + Dagger2 + Robolectric
優(yōu)點:測試速度快,正常情況快下都為秒級別
缺點:存在局限性,如JUnit+Mockito+MVP是在JVM上運行的,沒有Android的運行環(huán)境(沒有Android相關(guān)方法的具體實現(xiàn)),需要對Android有依賴的單元進(jìn)行依賴隔離,因此無法測試與Android相關(guān)的單元;JUnit + Mockito + Dagger2 + Robolectric雖然Robolectric模擬了Android環(huán)境,讓測試代碼在JVM中能夠測試Android相關(guān)的單元,但是Robolectric僅支持API21及以下,并且不支持JNI庫,當(dāng)被測類中涉及JNI(如百度地圖SDK)如果沒有進(jìn)行依賴隔離,測試類將會報錯,無法正常運行。依賴Android環(huán)境,需要運行在模擬器或真機(jī)上
如Android提供的Instrumentation測試框架、Espresso
優(yōu)點:測試的覆蓋面大,由于運行在模擬器或真機(jī)上,因此能夠測試與Android相關(guān)的單元
缺點:運行時間長,由于行在模擬器或真機(jī)上所以會經(jīng)歷打包和安裝的過程,導(dǎo)致消耗較多的時間
根據(jù)實際情況,可以靈活切換以上兩種方案
如何在Android中進(jìn)行單元測試
- 首先進(jìn)行相關(guān)的配置
在Android Studio中默認(rèn)情況下不需要進(jìn)行配置,已經(jīng)支持Instrumentation與純JUnit,分別在androidTest與test中創(chuàng)建測試類,編寫測試代碼

????在Eclipse中需要為被測工程添加JUnit依賴,在被測工程右鍵點擊Properties,在窗口左側(cè)選擇Java Build Path,選中右側(cè)Libraries,點擊Add Library,選擇JUnit


更好的做法是新建一個測試工程,將被測工程作為測試工程的依賴,再為測試工程進(jìn)行如上配置,方便我們對測試代碼的管理。
- 以下對JUnit單元測試進(jìn)行簡單介紹,基于Instrumentation的單元測試由于是對JUnit的擴(kuò)展就不過多介紹(其實是了解不夠深入)
一個單元測試大概可以分為三個部分:
setup:即new 出待測試的類,為測試設(shè)置一些前提條件
執(zhí)行動作:即調(diào)用被測類的被測方法,并獲取返回結(jié)果
驗證結(jié)果:驗證獲取的結(jié)果跟預(yù)期的結(jié)果是一樣的
代碼示例如下:
public class Calculator {
/**
* 將兩個數(shù)相加
* @param a
* @param b
* @return a + b
*/
public int add(int a,int b){
return a+b;
}
/**
* 將兩個數(shù)相減
* @param a 被減數(shù)
* @param b 減數(shù)
* @return a - b
*/
public int subtract(int a,int b){
//將被減數(shù)與減數(shù)互換,模擬Bug
return b - a ;
}
}
Calculator 為被測類,Calculator 中有兩個方法,也就是測試單元。add方法做加法計算、subtract方法做減法計算(subtract中將被減數(shù)與減數(shù)互換,模擬Bug)
public class JUnitTest {
private Calculator mCalculator;
@Before
public void setUp(){
mCalculator = new Calculator();
}
@Test
public void testAdd(){
int result = mCalculator.add(1, 3);
Assert.assertEquals(4, result);
}
@Test
public void testSubtract(){
int result = mCalculator.subtract(6, 4);
Assert.assertEquals(2, result);
}
}
JUnitTest 為測試類,該類的創(chuàng)建過程可與正常類創(chuàng)建過程一致。
其中以@Before注解的方法中的代碼對應(yīng)前文中提到的三個步驟中的setUp,為以@Test注解的測試方法設(shè)置一些共有的前提條件,在這個例子中就是new出被測試類。而實際情況中可能還有相關(guān)參數(shù)與配置相關(guān)依賴或通過Mock框架進(jìn)行依賴隔離等操作。
以@Test注解的方法之間是互相獨立的,不存在執(zhí)行上的因果關(guān)系
以testSubtract()為例
int result = mCalculator.subtract(6, 4);
對應(yīng)三個步驟中的執(zhí)行動作,即執(zhí)行Calculator中的add方法并獲得add方法的執(zhí)行結(jié)果
Assert.assertEquals(2, result);
對應(yīng)三個步驟中的驗證結(jié)果,Assert為JUnit提供的類,內(nèi)部有一系列用于驗證被測單元返回值是否與期望值一致的方法,在本例中通過Assert.assertEquals(4, result),驗證mCalculator.subtract(6, 4)的執(zhí)行結(jié)果result是否與預(yù)期值4相等
接下來就是運行測試類JUnitTest Android Studio中右鍵點擊 Run ‘JUnitTest ’ 會執(zhí)行JUnitTest 中所有以@Test注解的方法,并會輸出驗證報告
在Eclipse中需要進(jìn)行配置,才能進(jìn)行純Junit的單元測試,在被測類中右鍵點擊Run As,點擊Run Configurations

在出現(xiàn)的窗口中選中右側(cè)的Classpath,默認(rèn)情況下Bootstrap Entries節(jié)點下應(yīng)該為Android SDK,而這里需要把Android SDK替換為JRE System Library。替換流程如下圖,先將Bootstrap Entries節(jié)點下的Android SDK Remove,之后選中Bootstrap Entries節(jié)點,點擊右側(cè)的Advanced,選中Add Library,選擇JRE System Library,Next 直到結(jié)束



配置完成后就可以在被測類中右鍵點擊 Run As JUnit Test,運行完成之后就會輸出測試報告如下圖(下圖為Eclipse中的測試報告,Android Studio中類似)

從上往下看上圖,首先Failures表示有一個測試沒有通過,本例中的運行時間基本可以忽略不計為(0.019s),相對于運行到真機(jī)上差別是非常大的。testSubtract測試方法沒有通過,因為在被測方法中為了模擬Bug將減數(shù)與被減數(shù)互換,導(dǎo)致預(yù)期結(jié)果(6 - 4 = 2)expected:<2> 與實際運行結(jié)果(4 - 6 = -2)<-2>不一致.根據(jù)上圖我們就能快速的將Bug定位到被測類Calculator 的subtract方法中(快速定位Bug)。
在實際的項目代碼的情況會相對比較復(fù)雜,因此可以通將純Java的邏輯代碼抽離出來,具體方案有通過MVP架構(gòu)將邏輯代碼與Android 組件(比如:Activity)解耦,或者像上面的例子中將純Java邏輯代碼封裝到類似Calculator的Utils類中,不過要盡量避免使用靜態(tài)方法,這樣的訪問方式。(提升項目代碼質(zhì)量)
小結(jié)
這邊只簡單總結(jié)了我自己目前在學(xué)習(xí)Andorid單元測試中的感悟和收獲,Andorid單元測試中其實還涉及到很多其他的技術(shù),比如Mock的概念以及Mockito框架(隔離依賴,保證被測單元的獨立性)、Dagger2依賴注入框架,配合Mockito讓我們更便利的在Android中進(jìn)行單元測試、MVP架構(gòu)。
參考文獻(xiàn)
- Android單元測試: 首先,從是什么開始 http://chriszou.com/2016/04/13/android-unit-testing-start-from-what.html
- Android單元測試 - 如何開始? http://m.itdecent.cn/p/bc99678b1d6e
- Android單元測試 - 幾個重要問題 http://m.itdecent.cn/p/f5d197a4d83a
- 蘑菇街支付金融Android單元測試實踐 http://www.infoq.com/cn/articles/mogujie-android-unit-testing
