引言
這篇文章主要是總結一下我自己在學習Android單元測試過程中的收獲及感悟,同時也希望可以幫助到正在學習Android單元測試的小伙伴們.由于時間及經驗有限,文中可能存在錯誤與不足,歡迎大家指出,我會在第一時間對文章進行修改糾正.
本文主要包含以下內容:
- 什么是單元測試
- 為什么需要進行單元測試
- 如何進行單元測試
什么是單元測試
首先總結一下什么是單元測試,單元測試中的單元在Android或Java中可以理解為某個類中的某一個方法,因此單元測試就是針對Android或Java中某個類中的某一個方法中的邏輯代碼進行驗證即測試該方法是不是可以正常工作。
還有一點就是要區分單元測試與集成測試(功能測試、UI測試),單元測試是針對單元即方法的測試,被測單元粒度要小并且具備獨立性,而集成測試是測試多個單元(方法)組合成的功能模塊。
為什么需要進行單元測試
-
單元測試的測試相對于集成測試的測試成本較低
單元測試相對于集成測試有運行時間短、投入成本低的優勢即Test Pyramid理論:
Test Pyramid
從上圖可以看出單元測試,測試速度快投入成本少
因此我們要將大部分精力投放在單元測試中,保證單元測試的質量之后再進行集成測試與UI測試來提高測試效率 -
提高開發效率
開發Android App的小伙伴可能都會有這樣一個體會,就是當App項目逐漸增大,運行App進行調試會花費大量時間在項目的構建、編譯、打包、安裝上。這個過程的持續時間與App的規模成線性相關即App項目規模越大持續時間就越久。因此隨著我們的的項目逐漸增大,運行App的進行調試時,我們的調試成本也在逐漸增加。
而單元測試正好能解決這個問題。
舉個例子:
在登錄Activity中有個checkPhoneNum方法,這個方法的功能是在點擊登錄按鈕時,對用戶輸入的登錄賬號進行本地的合法性驗證避免不必要的網絡請求,如果是通過運行App來驗證checkPhoneNum方法是否能夠正常運行,需要經過構建、編譯、打包、安裝的過程,程序運行之后還需要人工操作進入登陸頁面,輸入賬號密碼,點擊登錄按鈕,觸發checkPhoneNum方法,這個過程可能需要幾十秒甚至一分多鐘,如果通過MVP架構將checkPhoneNum作為純Java代碼抽離出來,屏蔽對Android平臺的依賴,就能將單元測試運行在JVM上,并針對checkPhoneNum方法進行測試,免去了構建、編譯、打包、安裝的過程,整個驗證過程就在一秒之內,開發效率將大幅提升。(大致的測試流程在下個章節進行說明)
public boolean checkPhoneNum(String phoneNum){
//判斷phoneNum是否為空(實際的判斷會稍微復雜一點,為了舉例做了簡化)
if(phoneNum == null || "".equals(phoneNum)){
return false;
}
return true;
}
-
提升項目工程代碼質量
進行單元測試前提之一就是被測單元具備可測性,以上面checkPhoneNum方法為例,如果checkPhoneNum方法中的代碼直接寫在登錄按鈕的點擊事件中,而沒有抽取為checkPhoneNum方法,那么對這段代碼進行單元測試是會非常困難的,極端情況甚至無法測試。所以為了寫出可測試的代碼可以鍛煉開發人員對的代碼的抽象能力和加強對項目架構的把控,從而提升項目工程代碼質量。 -
快速定位Bug
由于單元測試對被測項目中的被測單元的獨立性的要求,因此在被測單元的執行結果與預期結果不一致時我們就能快速的定位到出現Bug的方法。(在下個章節中會舉例說明)
如何進行單元測試
在Android中進行單元測試有很多方案,主要可以分為兩類
在運行在JVM上,不依賴Android環境
如基礎的 JUnit+Mockito+MVP 或比較全面的JUnit + Mockito + Dagger2 + Robolectric
優點:測試速度快,正常情況快下都為秒級別
缺點:存在局限性,如JUnit+Mockito+MVP是在JVM上運行的,沒有Android的運行環境(沒有Android相關方法的具體實現),需要對Android有依賴的單元進行依賴隔離,因此無法測試與Android相關的單元;JUnit + Mockito + Dagger2 + Robolectric雖然Robolectric模擬了Android環境,讓測試代碼在JVM中能夠測試Android相關的單元,但是Robolectric僅支持API21及以下,并且不支持JNI庫,當被測類中涉及JNI(如百度地圖SDK)如果沒有進行依賴隔離,測試類將會報錯,無法正常運行。依賴Android環境,需要運行在模擬器或真機上
如Android提供的Instrumentation測試框架、Espresso
優點:測試的覆蓋面大,由于運行在模擬器或真機上,因此能夠測試與Android相關的單元
缺點:運行時間長,由于行在模擬器或真機上所以會經歷打包和安裝的過程,導致消耗較多的時間
根據實際情況,可以靈活切換以上兩種方案
如何在Android中進行單元測試
- 首先進行相關的配置
在Android Studio中默認情況下不需要進行配置,已經支持Instrumentation與純JUnit,分別在androidTest與test中創建測試類,編寫測試代碼
????在Eclipse中需要為被測工程添加JUnit依賴,在被測工程右鍵點擊Properties,在窗口左側選擇Java Build Path,選中右側Libraries,點擊Add Library,選擇JUnit
更好的做法是新建一個測試工程,將被測工程作為測試工程的依賴,再為測試工程進行如上配置,方便我們對測試代碼的管理。
- 以下對JUnit單元測試進行簡單介紹,基于Instrumentation的單元測試由于是對JUnit的擴展就不過多介紹(其實是了解不夠深入)
一個單元測試大概可以分為三個部分:
setup:即new 出待測試的類,為測試設置一些前提條件
執行動作:即調用被測類的被測方法,并獲取返回結果
驗證結果:驗證獲取的結果跟預期的結果是一樣的
代碼示例如下:
public class Calculator {
/**
* 將兩個數相加
* @param a
* @param b
* @return a + b
*/
public int add(int a,int b){
return a+b;
}
/**
* 將兩個數相減
* @param a 被減數
* @param b 減數
* @return a - b
*/
public int subtract(int a,int b){
//將被減數與減數互換,模擬Bug
return b - a ;
}
}
Calculator 為被測類,Calculator 中有兩個方法,也就是測試單元。add方法做加法計算、subtract方法做減法計算(subtract中將被減數與減數互換,模擬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 為測試類,該類的創建過程可與正常類創建過程一致。
其中以@Before注解的方法中的代碼對應前文中提到的三個步驟中的setUp,為以@Test注解的測試方法設置一些共有的前提條件,在這個例子中就是new出被測試類。而實際情況中可能還有相關參數與配置相關依賴或通過Mock框架進行依賴隔離等操作。
以@Test注解的方法之間是互相獨立的,不存在執行上的因果關系
以testSubtract()為例
int result = mCalculator.subtract(6, 4);
對應三個步驟中的執行動作,即執行Calculator中的add方法并獲得add方法的執行結果
Assert.assertEquals(2, result);
對應三個步驟中的驗證結果,Assert為JUnit提供的類,內部有一系列用于驗證被測單元返回值是否與期望值一致的方法,在本例中通過Assert.assertEquals(4, result),驗證mCalculator.subtract(6, 4)的執行結果result是否與預期值4相等
接下來就是運行測試類JUnitTest Android Studio中右鍵點擊 Run ‘JUnitTest ’ 會執行JUnitTest 中所有以@Test注解的方法,并會輸出驗證報告
在Eclipse中需要進行配置,才能進行純Junit的單元測試,在被測類中右鍵點擊Run As,點擊Run Configurations
在出現的窗口中選中右側的Classpath,默認情況下Bootstrap Entries節點下應該為Android SDK,而這里需要把Android SDK替換為JRE System Library。替換流程如下圖,先將Bootstrap Entries節點下的Android SDK Remove,之后選中Bootstrap Entries節點,點擊右側的Advanced,選中Add Library,選擇JRE System Library,Next 直到結束
配置完成后就可以在被測類中右鍵點擊 Run As JUnit Test,運行完成之后就會輸出測試報告如下圖(下圖為Eclipse中的測試報告,Android Studio中類似)
從上往下看上圖,首先Failures表示有一個測試沒有通過,本例中的運行時間基本可以忽略不計為(0.019s),相對于運行到真機上差別是非常大的。testSubtract測試方法沒有通過,因為在被測方法中為了模擬Bug將減數與被減數互換,導致預期結果(6 - 4 = 2)expected:<2> 與實際運行結果(4 - 6 = -2)<-2>不一致.根據上圖我們就能快速的將Bug定位到被測類Calculator 的subtract方法中(快速定位Bug)。
在實際的項目代碼的情況會相對比較復雜,因此可以通將純Java的邏輯代碼抽離出來,具體方案有通過MVP架構將邏輯代碼與Android 組件(比如:Activity)解耦,或者像上面的例子中將純Java邏輯代碼封裝到類似Calculator的Utils類中,不過要盡量避免使用靜態方法,這樣的訪問方式。(提升項目代碼質量)
小結
這邊只簡單總結了我自己目前在學習Andorid單元測試中的感悟和收獲,Andorid單元測試中其實還涉及到很多其他的技術,比如Mock的概念以及Mockito框架(隔離依賴,保證被測單元的獨立性)、Dagger2依賴注入框架,配合Mockito讓我們更便利的在Android中進行單元測試、MVP架構。
參考文獻
- Android單元測試: 首先,從是什么開始 http://chriszou.com/2016/04/13/android-unit-testing-start-from-what.html
- Android單元測試 - 如何開始? http://www.lxweimin.com/p/bc99678b1d6e
- Android單元測試 - 幾個重要問題 http://www.lxweimin.com/p/f5d197a4d83a
- 蘑菇街支付金融Android單元測試實踐 http://www.infoq.com/cn/articles/mogujie-android-unit-testing