在Android項目中實施單元測試,是近幾年來相關的討論有很多,谷歌官方也提供了一些方案,但是網上很多文章往往都只有最簡單的項目demo漩怎,對于復雜項目幾乎無任何實踐參考價值焊傅。因此本文總結了一下最近的調研,嘗試找到一種能讓單元測試在安卓項目中落地的姿勢榕茧。
文章主要分成
調研努潘、
實踐
兩篇诽偷。
本篇主要講講Android單元測試的調研情況。
0 收益
單元測試(unit testing)疯坤,是指對軟件中的最小可測試單元進行檢查和驗證报慕。什么是最小可測單元——這是人為劃分的,可以是一個類压怠、函數或者可視化的某個功能眠冈。
很重要的一點是,單元測試強調 被測的獨立單元要與程序的其他部分相隔離的情況下進行測試菌瘫。
那么單元測試能為我們帶來什么收益——或者說我們?yōu)槭裁促M時費力要進行單元測試蜗顽?
主要是以下3點:
1. 保證業(yè)務交付質量.
單元測試對項目進行函數級別的測試,一方面可以測試各種邊界雨让、極限條件下代碼的健壯性诫舅,同時能夠在迭代過程中檢查出代碼改動帶來的不穩(wěn)定因素,降低團隊高速迭代的風險宫患。
2. 單元測試迫使我們去做封裝和改造,讓項目可以更優(yōu)雅地進行測試这弧。
你會很自覺地 把每個類寫的比較小娃闲,
你會把每個功能職責分配的很清楚,而不是把一堆代碼都塞到一個類里面(比如Activity)匾浪。
你會自覺地更偏向于采用組合皇帮,而不是繼承的方式去寫代碼。
3. 單元測試的case類似于技術文檔蛋辈,具備業(yè)務指導能力属拾。
單元測試用例往往和某個頁面的具體業(yè)務流程以及代碼邏輯 緊密聯系将谊,所以單元測試用例可以像文檔一樣具備業(yè)務指導能力。
退一萬步講渐白,單元測試提供了一種快速便捷的方式讓我們可以去測試某個模塊/函數的邏輯是否足夠健壯尊浓,而不是在項目業(yè)務里侵入式地加一些測試代碼。那么這個單元測試相關框架的引入的收益就已經足夠大了纯衍。
1 框架調研
在新建工程時栋齿,可以看到src目錄下有androitTest和test兩個目錄,二者都是Android測試代碼的目錄襟诸,但有所不同:
- /src/androidTest的代碼需要運行在真機/模擬器上瓦堵,主要是測某個功能是否正常,類似于UI自動化測試歌亲。
- /src/test的代碼可以直接運行在JVM上菇用,可以驗證函數級別的邏輯,就是我們一般所說的單元測試代碼陷揪。
所以說Android的測試代碼分為 運行在真機和JVM上兩類惋鸥,下面介紹下相關的幾個框架:
- JUnit,Java單元測試的根基鹅龄,基本上都是通過斷言來驗證函數返回值/對象的狀態(tài)是否正確揩慕。
- Espresso,谷歌官方提供的UI自動化測試框架扮休,需要運行在手機/模擬器上迎卤,類似于Appium。
- Robolectric玷坠,實現了一套可以在JVM上運行的Android代碼蜗搔。
- Mockito,如果被測的業(yè)務依賴比較復雜的上下文八堡,就可以使用Mock來模擬被測代碼依賴的對象來保證單元測試的進行樟凄。
下面講講幾個框架的調研情況及取舍,趕時間的可以直接看文末結論兄渺。
1.1 JUnit
JUnit是Java單元測試的根基缝龄,測試用例的運行和驗證都依賴于它來進行。Android使用Java語言開發(fā)挂谍,Android單元測試自然離不開JUnit叔壤。
JUnit的用途主要是:
- 提供了若干注解,輕松地組織和運行測試口叙。
- 提供了各種斷言api炼绘,用于驗證代碼運行是否符合預期。
斷言的api不做介紹了妄田,自行查閱官方wiki俺亮。
簡單介紹一下幾個常用注解:
- @Test
標記該方法為測試方法驮捍。測試方法必須是public void,可以拋出異常脚曾。 - @RunWith
指定一個Runner來提供測試代碼運行的上下文環(huán)境东且。(Runner的概念) - @Rule
定義測試類中的每個測試方法的行為,比如指定某個Acitivity作為測試運行的上下文斟珊。 - @Before
初始化方法苇倡,通常進行測試前置條件的設置。 - @After
釋放資源囤踩,它會在每個測試方法執(zhí)行完后都調用一次旨椒。
@RunWith(JUnit4.class)
public class JUnitSample {
Object object;
//初始化方法,通常進行用于測試的前置條件/依賴對象的初始化
@Before
public void setUp() throws Exception {
object = new Object();
}
//測試方法堵漱,必須是public void
@Test
public void test() {
Assert.assertNotNull(object);
}
}
ps: 一個測試類單元測試的執(zhí)行順序為: @BeforeClass –> @Before –> @Test –> @After –> @AfterClass
結論:JUnit是單元測試的根基综慎。
1.2 Espresso
谷歌官方的UI自動化測試框架,用Espresso寫的測試代碼勤庐,必須跑在emulator或者是device上面示惊,并且在測試代碼的運行過程中,也會真正的拉起頁面愉镰、發(fā)生UI交互米罚、文件讀寫、網絡請求等等丈探,最后通過各種斷言檢查UI狀態(tài)录择。
框架提供了以下三類api:
- ViewMatchers,找出被測的View對象碗降,相當于在測試代碼中實現了findViewById隘竭。
- ViewActions,發(fā)送交互事件讼渊,即在測試代碼中模擬UI觸摸交互动看。
- ViewAssertions,驗證UI狀態(tài)爪幻,在測試代碼運行完成后檢查UI狀態(tài)是否符合預期菱皆,可以看做是UI狀態(tài)的斷言。
話不多說挨稿,直接看簡單demo:
//使用Espresso提供的AndroidJUnit4運行測試代碼
@RunWith(AndroidJUnit4.class)
public class EspressoSample {
// 利用Espresso提供的ActivityTestRule拉起MainActivity
@Rule
public ActivityTestRule<MainActivity> mIntentsRule = new IntentsTestRule<>(MainActivity.class);
@Test
public void testNoContentView() throws Exception {
//withId函數會返回一個ViewMatchers對象仇轻,用于查找id為R.id.btn_get的view
onView(withId(R.id.btn_get))
//click函數會返回一個ViewActions對象,用于發(fā)出點擊事件
.perform(click());
//通過定時輪詢loadingView是否展示中叶组,來判斷異步的網絡請求是否完成
View loadingView = mIntentsRule.getActivity().findViewById(R.id.loading_view);
while (true) {
Thread.sleep(1000);
if (loadingView.getVisibility() == View.GONE) {
break;
}
}
//請求請求完成后,檢查UI狀態(tài)
//找到R.id.img_result的view
onView(withId(R.id.img_result))
//matches函數會返回一個ViewAssertions對象历造,檢查這個view的某個狀態(tài)是否符合預期
.check(matches(isDisplayed()));
}
}
以上測試代碼需要運行在真機/模擬器上甩十,運行過程中可以看到自動拉起MainActivity船庇,并且自動點擊了id為btn_get的按鈕,然后loading結束后侣监,檢查到id為img_result正在展示中鸭轮,符合預期,整個測試用例就執(zhí)行成功了橄霉。
可以感覺到Espresso的確比較強大窃爷,通過其提供的api,常用的UI邏輯基本都可以進行測試姓蜂。但在復雜項目中按厘,Espreeso的缺點也非常明顯:
1. 粒度粗。
Espresso本質上就是一種UI自動化測試方案钱慢,很難去驗證函數級別的邏輯逮京,如果僅僅是想驗證某個功能是否正常的話,又受限于網絡狀況束莫、設備條件甚至用戶賬戶等等因素懒棉,測試結果不可控。
2. 邏輯復雜览绿。
一般頁面UI元素龐大且復雜策严,不可能每個View的交互邏輯都去寫測試代碼驗證,只能選擇性驗證一些關鍵交互饿敲。
3. 運行速度慢妻导。
用Espresso寫測試代碼,必須跑在emulator或者是device上面诀蓉。運行測試用例就變成了一個漫長的過程栗竖,因為要打包、上傳到機器渠啤、然后再一個一個地運行UI界面狐肢,這樣做的好處是手機上的表現很直觀,但是調試和運行速度是真的慢沥曹,效率和便捷性上肯定是不如人工測試份名。
結論:Espresso用例的編寫就像是在做業(yè)務代碼的逆向實現,在實際工作中還不如直接運行項目代碼進行人工自測妓美,所以個人感覺Espresso是一個強大的UI自動化測試工具僵腺,而非單元測試的解決方案。
1.3 Robolectric
Espresso的問題很明顯壶栋,那么有沒有可能讓Android代碼脫離手機/模擬器辰如,直接運行在JVM上面呢?
我們需要一個能夠隔離Android依賴贵试,并且能夠 直接在IDE里run一下就可以知道結果的單元測試方案琉兜。
這就牽涉到android.jar的問題凯正,android.jar包含了Android Framework的所有類、函數豌蟋、變量的聲明廊散,但它沒有任何具體實現,android.jar僅僅用于JAVA代碼編譯時梧疲,并不會真正打包進APK允睹,Android Framework的真正實現是在設備/模擬器上。在JVM上調用Android SDK里的函數會直接throw RuntimeException幌氮。
所以Android單元測試需要解決的一大痛點缭受,就是如何隔離整個Android SDK的依賴。
谷歌官方推薦的開源測試框架 Robolectric就是這么一個工具浩销,簡單來說它實現了一套可以在JVM上運行的Android代碼贯涎。
谷歌官方推薦的開源測試框架 Robolectric就是這么一個工具,它實現了一套可以在JVM上運行的Android代碼慢洋。
Shadow
是Robolectric的核心塘雳,這個框架針對Android SDK中的對象,提供了很多影子對象(如Activity
和ShadowActivity
普筹、TextView
和ShadowTextView
等)败明,Robolectric的本質是在Java運行環(huán)境下,采用Shadow的方式對Android中的組件進行模擬測試太防,從而實現Android單元測試妻顶。對于一些Robolectirc暫不支持的組件,可以采用自定義Shadow的方式擴展Robolectric的功能蜒车。
由于Robolectric坑太多(可能是我道行不夠)讳嘱,就不放簡單demo介紹api了(主要是我跑不通demo),直接說說它的坑吧:
- Robolectric版本和Android SDK版本強依賴酿愧。Robolectric會shadow大部分Android的代碼
版本分散且缺少說明 - 首次啟動Robolectric會下載maven相關的依賴失敗沥潭。這個依賴的文件較大,且下載邏輯是寫在Robolectric框架里的嬉挡,不能通過網絡代理的方式解決钝鸽,網上有一些解決方案,但在新版本的Robolectric里都已經失效了庞钢。
- 不兼容第三方庫拔恰。大量的第三方庫并沒有對應的shadow類,會在啟動時/運行時報錯基括。
- 靜態(tài)代碼塊易報錯颜懊。我們經常在靜態(tài)代碼塊里去加載so庫或者執(zhí)行一些初始化邏輯,基本上都會報錯且很難解決。如果為了這個單元測試反過來去修改這些邏輯河爹,就顯得有點本末倒置使鹅、得不償失了。
國外關于Robolectri也有不少討論:https://www.philosophicalhacker.com/post/why-i-dont-use-roboletric/
結論:當被測的代碼(Presenter昌抠、Model層等)不可避免的依賴了Android SDK代碼(比如TextUtils、Looper等),Robolectric可以輕松地讓測試代碼跑在JVM上鲁僚,這應該是Robolectric的最大意義了炊苫。但是因為上述幾點的情況,當連成功運行代碼都成為了一種奢望冰沙,我不覺得這么一個單元測試框架能夠在項目落地侨艾。
1.4 Mock
剛剛說到 Espresso需要跑在真機上,Robolectric問題太多在復雜項目中寸步難行拓挥。
所以可以考慮使用Mock框架來隔離整個Android SDK以及項目業(yè)務的依賴唠梨,將單元測試的重心放在函數級別的代碼邏輯上。
Mock的定義是創(chuàng)造一些模擬對象/數據來測試程序的行為侥啤。
平時我們接觸的最多的就是Mock Server当叭,就是模擬接口返回數據提供給前端調試。
但在單元測試中盖灸,如果被測的業(yè)務依賴比較復雜的上下文蚁鳖,就可以使用Mock創(chuàng)造模擬代碼里的對象來保證單元測試的進行。
類似汽車設計者使用碰撞測試假人來模擬車輛碰撞中人的受傷情況赁炎。
Mock框架基本上是以下2個:
- Mockito
- 模擬對象并使其按照我們預期執(zhí)行/返回(類似代碼打樁)
- 驗證模擬對象是否按照預期執(zhí)行/返回
- PowerMockito
- 基于Mockito
- 支持模擬靜態(tài)函數醉箕、構造函數、私有函數徙垫、final 函數以及系統(tǒng)函數
PowerMockito非常強大讥裤,但PowerMock使用的越多,表示被測試的代碼抽象層次越低姻报,代碼質量和結構也越差己英,沒關系,有點歷史的大型項目都是類似的情況逗抑。
因為PowerMockito是基于Mockito的擴展剧辐,所以二者的api都非常相似,常用api是以下兩類:
- 模擬對象并指定某些函數的執(zhí)行/返回值
when(...).thenReturn(...)
- 驗證模擬對象是否按照預期執(zhí)行/返回
verify(...).invoke(...)
下面講講單元測試中如何借助PowerMockito來隔離Android SDK和項目業(yè)務的依賴:
- Mock被依賴的復雜對象
- 執(zhí)行被測代碼
- 驗證邏輯是否按照預期執(zhí)行/返回
public class PowerMockitoSample {
private MainActivity activity;
private ImageView mockImg;
private TextView mockTv;
@Before
public void setUp() {
activity = new MainActivity();
// 1. Mock被依賴的復雜對象邮府。
// MainActivity依賴了一些View荧关,下面就是Mock出被依賴的復雜對象,并使之成為MainActivity的私有變量
mockImg = PowerMockito.mock(ImageView.class);
Whitebox.setInternalState(activity, "resultImg", mockImg);
mockTv = PowerMockito.mock(TextView.class);
Whitebox.setInternalState(activity, "resultTv", mockTv);
Whitebox.setInternalState(activity, "loadingView", PowerMockito.mock(ProgressBar.class));
}
@Test
public void test_onFail() throws Exception {
// 2. 執(zhí)行被測代碼褂傀。
// 這里要驗證activity.onFail()函數
String errorMessage = "test";
activity.onFail(errorMessage);
// 3. 驗證邏輯是否按照預期執(zhí)行/返回忍啤。
// 這里需要驗證resultImg 和 resultTv有沒有按照預期進行UI狀態(tài)的改變
verify(mockImg).setImageResource(R.drawable.ic_error);
verify(mockTv).setText(errorMessage);
}
}
上面代碼我們把MainActivity所依賴的各種View對象通過mock實現后,剩下的基本都是工作量的問題了。
可以看到同波,借助Mock框架可以很好的隔離復雜的依賴對象(比如View)鳄梅,從而保證被測的獨立單元可以與程序的其他部分相隔離的情況下進行測試,然后專注于驗證某個函數/模塊的邏輯是否正確且健壯未檩。
必須注意的是戴尸,在實際項目中會有很多常用但不影響業(yè)務邏輯的代碼(Log以及其他統(tǒng)計代碼等),部分靜態(tài)代碼塊也直接調用Android SDK api冤狡。因為單元測試代碼運行在JVM上孙蒙,需要抑制/隔離這些代碼的執(zhí)行,PowerMockito都提供了不錯的支持(下篇細說)悲雳。
結論:通過PowerMockito這種強大的Mock框架挎峦,將被測類所依賴的復雜對象直接代理掉,既不會要求侵入式地修改業(yè)務代碼 也能夠保證單元測試代碼 快速有效地運行在JVM上合瓢,
2 結論
- JUnit是基礎坦胶。
- Espresso需要跑在真機上,可用于依賴Android平臺的功能測試而非單元測試晴楔。
- Roboelctric問題太多在復雜項目中寸步難行顿苇,棄了。
- Android單元測試主要是通過PowerMockito來隔離整個Android SDK以及項目業(yè)務的依賴税弃,將單元測試的重心放在較細粒度(函數級別)的代碼邏輯上岖圈。