Android單元測試在複雜項目裏的落地姿式(調研篇)

代碼出處:colin-phang/AndroidUnitTest前端

下篇文章:Android單元測試在複雜項目裏的落地姿式(PowerMock實踐篇)java

在Android項目中實施單元測試,是近幾年來相關的討論有不少,谷歌官方也提供了一些方案,可是網上不少文章每每都只有最簡單的項目demo,對於複雜項目幾乎無任何實踐參考價值。所以本文總結了一下最近的調研,嘗試找到一種能讓單元測試在安卓項目中落地的姿式。android

文章主要分紅 調研、 實踐 兩篇。 本篇主要講講Android單元測試的調研狀況。git

0 收益

單元測試(unit testing),是指對軟件中的最小可測試單元進行檢查和驗證。什麼是最小可測單元——這是人爲劃分的,能夠是一個類、函數或者可視化的某個功能。 很重要的一點是,單元測試強調 被測的獨立單元要與程序的其餘部分相隔離的狀況下進行測試。github

那麼單元測試能爲咱們帶來什麼收益——或者說咱們爲何費時費力要進行單元測試? 主要是如下3點:api

1. 保證業務交付質量.markdown

單元測試對項目進行函數級別的測試,一方面能夠測試各類邊界、極限條件下代碼的健壯性,同時可以在迭代過程當中檢查出代碼改動帶來的不穩定因素,下降團隊高速迭代的風險。網絡

2. 單元測試迫使咱們去作封裝和改造,讓項目能夠更優雅地進行測試。框架

你會很自覺地 把每一個類寫的比較小, 你會把每一個功能職責分配的很清楚,而不是把一堆代碼都塞到一個類裏面(好比Activity)。 你會自覺地更偏向於採用組合,而不是繼承的方式去寫代碼。異步

3. 單元測試的case相似於技術文檔,具有業務指導能力。

單元測試用例每每和某個頁面的具體業務流程以及代碼邏輯 緊密聯繫,因此單元測試用例能夠像文檔同樣具有業務指導能力。

退一萬步講,單元測試提供了一種快速便捷的方式讓咱們能夠去測試某個模塊/函數的邏輯是否足夠健壯,而不是在項目業務裏侵入式地加一些測試代碼。那麼這個單元測試相關框架的引入的收益就已經足夠大了。

1 框架調研

1.0 單元測試的範圍

哪些函數須要進行單元測試?能夠簡單歸納爲如下三種:

  1. 有明確的返回值,只需調用這個函數,而後驗證函數的返回值是否符合預期結果。
  2. 這個函數自己沒有返回值,就驗證它所改變的屬性和狀態。
  3. 一些函數沒有返回值,也沒有直接改變哪一個值的狀態,這就須要驗證其行爲,好比發生了頁面跳轉、拉起相機。
  4. 既沒有返回值,也沒有改變狀態,又沒有觸發行爲的函數是不可測試的,在項目中不該該存在。

**框架的調研也是爲了儘量便捷地在Android項目裏覆蓋上述3種函數。**下面說說在Android開發中的單元測試的狀況。

在新建工程時,能夠看到src目錄下有androitTest和test兩個目錄,兩者都是Android測試代碼的目錄,但有所不一樣:

  1. /src/androidTest的代碼須要運行在真機/模擬器上,主要是測某個功能是否正常,相似於UI自動化測試。
  2. /src/test的代碼能夠直接運行在JVM上,能夠驗證函數級別的邏輯,就是咱們通常所說的單元測試代碼。

因此說Android的測試代碼分爲 運行在真機和JVM上兩類,下面介紹下相關的幾個框架:

  1. JUnit,Java單元測試的根基,基本上都是經過斷言來驗證函數返回值/對象的狀態是否正確。
  2. Espresso,谷歌官方提供的UI自動化測試框架,須要運行在手機/模擬器上,相似於Appium。
  3. Robolectric,實現了一套能夠在JVM上運行的Android代碼。
  4. Mockito,若是被測的業務依賴比較複雜的上下文,就可使用Mock來模擬被測代碼依賴的對象來保證單元測試的進行。

下面講講幾個框架的調研狀況及取捨,趕時間的能夠直接看文末結論。

1.1 JUnit

JUnit是Java單元測試的根基,測試用例的運行和驗證都依賴於它來進行。Android使用Java語言開發,Android單元測試天然離不開JUnit。 JUnit的用途主要是:

  1. 提供了若干註解,輕鬆地組織和運行測試。
  2. 提供了各類斷言api,用於驗證代碼運行是否符合預期。

斷言的api不作介紹了,自行查閱官方wiki

簡單介紹一下幾個經常使用註解:

  1. @Test

標記該方法爲測試方法。測試方法必須是public void,能夠拋出異常。 2. @RunWith 指定一個Runner來提供測試代碼運行的上下文環境。(Runner的概念) 3. @Rule 定義測試類中的每一個測試方法的行爲,好比指定某個Acitivity做爲測試運行的上下文。 4. @Before 初始化方法,一般進行測試前置條件的設置。 5. @After 釋放資源,它會在每一個測試方法執行完後都調用一次。

@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: 一個測試類單元測試的執行順序爲: @BeforeClass –> @Before –> @Test –> @After –> @AfterClass

結論:JUnit是單元測試的根基。

1.2 Espresso

谷歌官方的UI自動化測試框架,用Espresso寫的測試代碼,必須跑在emulator或者是device上面,而且在測試代碼的運行過程當中,也會真正的拉起頁面、發生UI交互、文件讀寫、網絡請求等等,最後經過各類斷言檢查UI狀態。 框架提供瞭如下三類api:

  1. ViewMatchers,找出被測的View對象,至關於在測試代碼中實現了findViewById。
  2. ViewActions,發送交互事件,即在測試代碼中模擬UI觸摸交互。
  3. ViewAssertions,驗證UI狀態,在測試代碼運行完成後檢查UI狀態是否符合預期,能夠看作是UI狀態的斷言。

話很少說,直接看簡單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對象,用於發出點擊事件
                .perform(click());  

        //經過定時輪詢loadingView是否展現中,來判斷異步的網絡請求是否完成
        View loadingView = mIntentsRule.getActivity().findViewById(R.id.loading_view);
        while (true) {
            Thread.sleep(1000);
            if (loadingView.getVisibility() == View.GONE) {
                break;
            }
        }

        //請求請求完成後,檢查UI狀態
        //找到R.id.img_result的view
        onView(withId(R.id.img_result))
                //matches函數會返回一個ViewAssertions對象,檢查這個view的某個狀態是否符合預期
                .check(matches(isDisplayed())); 
    }
}
複製代碼

以上測試代碼須要運行在真機/模擬器上,運行過程當中能夠看到自動拉起MainActivity,而且自動點擊了id爲btn_get的按鈕,而後loading結束後,檢查到id爲img_result正在展現中,符合預期,整個測試用例就執行成功了。

能夠感受到Espresso的確比較強大,經過其提供的api,經常使用的UI邏輯基本均可以進行測試。但在複雜項目中,Espreeso的缺點也很是明顯:

1. 粒度粗。

Espresso本質上就是一種UI自動化測試方案,很難去驗證函數級別的邏輯,若是僅僅是想驗證某個功能是否正常的話,又受限於網絡情況、設備條件甚至用戶帳戶等等因素,測試結果不可控。

2. 邏輯複雜。

通常頁面UI元素龐大且複雜,不可能每一個View的交互邏輯都去寫測試代碼驗證,只能選擇性驗證一些關鍵交互。

3. 運行速度慢。

用Espresso寫測試代碼,必須跑在emulator或者是device上面。運行測試用例就變成了一個漫長的過程,由於要打包、上傳到機器、而後再一個一個地運行UI界面,這樣作的好處是手機上的表現很直觀,可是調試和運行速度是真的慢,效率和便捷性上確定是不如人工測試。

結論:Espresso用例的編寫就像是在作業務代碼的逆向實現,在實際工做中還不如直接運行項目代碼進行人工自測,因此我的感受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中的對象,提供了不少影子對象(如ActivityShadowActivityTextViewShadowTextView等),Robolectric的本質是在Java運行環境下,採用Shadow的方式對Android中的組件進行模擬測試,從而實現Android單元測試。對於一些Robolectirc暫不支持的組件,能夠採用自定義Shadow的方式擴展Robolectric的功能。

因爲Robolectric坑太多(多是我道行不夠),就不放簡單demo介紹api了(主要是我跑不通demo),直接說說它的坑吧:

  1. Robolectric版本和Android SDK版本強依賴。Robolectric會shadow大部分Android的代碼

版本分散且缺乏說明 2. 首次啓動Robolectric會下載maven相關的依賴失敗。這個依賴的文件較大,且下載邏輯是寫在Robolectric框架裏的,不能經過網絡代理的方式解決,網上有一些解決方案,但在新版本的Robolectric裏都已經失效了。 3. 不兼容第三方庫。大量的第三方庫並無對應的shadow類,會在啓動時/運行時報錯。 4. 靜態代碼塊易報錯。咱們常常在靜態代碼塊裏去加載so庫或者執行一些初始化邏輯,基本上都會報錯且很難解決。若是爲了這個單元測試反過來去修改這些邏輯,就顯得有點本末倒置、得不償失了。

國外關於Robolectri也有很多討論:www.philosophicalhacker.com/post/why-i-…

結論:當被測的代碼(Presenter、Model層等)不可避免的依賴了Android SDK代碼(好比TextUtils、Looper等),Robolectric能夠輕鬆地讓測試代碼跑在JVM上,這應該是Robolectric的最大意義了。可是由於上述幾點的狀況,當連成功運行代碼都成爲了一種奢望,我不以爲這麼一個單元測試框架可以在項目落地。

1.4 Mock

剛剛說到 Espresso須要跑在真機上,Robolectric問題太多在複雜項目中步履維艱。 因此能夠考慮使用Mock框架來隔離整個Android SDK以及項目業務的依賴,將單元測試的重心放在函數級別的代碼邏輯上。

Mock的定義是創造一些模擬對象/數據來測試程序的行爲。 平時咱們接觸的最多的就是Mock Server,就是模擬接口返回數據提供給前端調試。

但在單元測試中,若是被測的業務依賴比較複雜的上下文,就可使用Mock創造模擬代碼裏的對象來保證單元測試的進行。 相似汽車設計者使用碰撞測試假人來模擬車輛碰撞中人的受傷狀況。

Mock框架基本上是如下2個:

  1. Mockito
    • 模擬對象並使其按照咱們預期執行/返回(相似代碼打樁)
    • 驗證模擬對象是否按照預期執行/返回
  2. PowerMockito
    • 基於Mockito
    • 支持模擬靜態函數、構造函數、私有函數、final 函數以及系統函數

PowerMockito很是強大,但PowerMock使用的越多,表示被測試的代碼抽象層次越低,代碼質量和結構也越差,不要緊,有點歷史的大型項目都是相似的狀況。 由於PowerMockito是基於Mockito的擴展,因此兩者的api都很是類似,經常使用api是如下兩類:

  1. 模擬對象並指定某些函數的執行/返回值
when(...).thenReturn(...)
複製代碼
  1. 驗證模擬對象是否按照預期執行/返回
verify(...).invoke(...)
複製代碼

下面講講單元測試中如何藉助PowerMockito來隔離Android SDK和項目業務的依賴:

  1. Mock被依賴的複雜對象
  2. 執行被測代碼
  3. 驗證邏輯是否按照預期執行/返回
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. 執行被測代碼。
        // 這裏要驗證activity.onFail()函數
        String errorMessage = "test";
        activity.onFail(errorMessage);
        // 3. 驗證邏輯是否按照預期執行/返回。
        // 這裏須要驗證resultImg 和 resultTv有沒有按照預期進行UI狀態的改變
        verify(mockImg).setImageResource(R.drawable.ic_error);
        verify(mockTv).setText(errorMessage);
    }
}
複製代碼

上面代碼咱們把MainActivity所依賴的各類View對象經過mock實現後,剩下的基本都是工做量的問題了。

能夠看到,藉助Mock框架能夠很好的隔離複雜的依賴對象(好比View),從而保證被測的獨立單元能夠與程序的其餘部分相隔離的狀況下進行測試,而後專一於驗證某個函數/模塊的邏輯是否正確且健壯。

必須注意的是,在實際項目中會有不少經常使用但不影響業務邏輯的代碼(Log以及其餘統計代碼等),部分靜態代碼塊也直接調用Android SDK api。由於單元測試代碼運行在JVM上,須要抑制/隔離這些代碼的執行,PowerMockito都提供了不錯的支持(下篇細說)。

結論:經過PowerMockito這種強大的Mock框架,將被測類所依賴的複雜對象直接代理掉,既不會要求侵入式地修改業務代碼 也可以保證單元測試代碼 快速有效地運行在JVM上,

2 結論

  1. JUnit是基礎。
  2. Espresso須要跑在真機上,可用於依賴Android平臺的功能測試而非單元測試。
  3. Roboelctric問題太多在複雜項目中步履維艱,棄了。
  4. Android單元測試主要是經過PowerMockito來隔離整個Android SDK以及項目業務的依賴,將單元測試的重心放在較細粒度(函數級別)的代碼邏輯上。
相關文章
相關標籤/搜索