1. 程式人生 > >Android單元測試實踐

Android單元測試實踐

為什麼要引入單元測試

一般來說我們都不會寫單元測試,為什麼呢?因為要寫多餘的程式碼,而且還要進行一些學習,入門有些門檻,所以一般在工程中都不會寫單元測試。那麼為什麼我決定要寫單元測試。因為兩個條件

  1. 我很懶:我每次改完都很懶測試
  2. 我很慫:我要是不測試,沒有一次通過的信心,於是我還是要測試。。。

這篇部落格看完並不會讓你完全掌握單元測試,但是會給你在單元測試的開始有一個好的指引

大大提高工作效率

單元的概念比較模糊,可以是一個方法,可以是一個時機,但是不是一整套環節,一整套環節那就是整合測試了。為什麼說大大提高了工作效率。有以下幾個場景

  1. 一個計算數字的方法你發現你寫錯了,你把+寫成了*,處於責任心,測試一下。首先你點選build,然後登上幾分鐘,期間可以喝個茶,看個朋友圈。。。。。然後,你發現你寫成了/,再來一次吧少年。。。。
  2. 你要修改一個專案,這時候你改了一個函式的小細節,線上crash了。。。
  3. 你在原來的基礎上改了一下業務,有一個一萬人只有一個人用的場景你不知道,UI出Bug了

等等等等,這些場景在普通的測試中無法很好的發現,畢竟你不能讓QA每次都把之前的業務都測試一下。所以這種場景如何解決?

寫單元測試

你可以測試一個方法而不用再跑一次程式,專案寫大了build一次可是很耗時的。寫好的單元測試也不用刪除,ci會幫你每次執行一下,來保證之前的邏輯是沒有問題的。

單元測試框架的選擇

Android的單元測試框架林林總總,目測10多種,所以,挑選是個問題,我總認為google官方推薦的和大眾都選擇使用的較好。所以我推薦(放心用,絕對是主流套裝)

    //local test
    testCompile 'junit:junit:4.12'
    testCompile 'org.mockito:mockito-core:2.7.22'
    testCompile 'org.robolectric:robolectric:3.3.2'
    //android test
    androidTestCompile 'org.mockito:mockito-core:2.7.22'
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2'
, { exclude group: 'com.android.support', module: 'support-annotations' })
  1. JUnit4:基礎的單元測試使用
  2. espresso:UI測試使用
  3. Mockito:Mock你要測試的類,
  4. Robolectric(可選):讓你在JVM環境中模擬Android的環境,這裡稍後解釋為什麼可選

解釋一下依賴,首先測試分成兩種

  1. 本地單元測試: 位於 module-name/src/test/java/

不需要Android環境的測試用例寫在這裡

  1. Android Instrumentation測試: 位於 module-name/src/androidTest/java/。

需要使用Android環境的測試用例寫在這裡,例如你要使用TextUtils.isEmpty(“”),這個函式是android包中的,需要寫在這個包下,寫在上面執行時會報錯。

引用自 Android官方文件

依次解釋

  • JUnit4是寫測試用例的,所有的測試用例都是用JUnit4來寫,所以這是基礎庫,不解釋。
  • espresso是Google官方的UI測試庫,可以對UI做白盒測試
  • Mockito是用來做Mock的,並且Mockito2.6+已經支援Android Instrumentation下使用什麼是Mock?
  • Robolectric是可以在JVM上模擬Android環境,也就是你可以在本地單元測試中使用這個庫模擬Android環境來做到呼叫Android Api的測試,為什麼要用呢?因為可能你在CI中進行測試你無法讓系統開啟一個Android模擬器或者真機來進行儀器測試,這也就是為什麼這個庫可選,你如果不用這個場景,就是不啟動虛擬機器才測試,你也就不用引入這個庫。而且打包使用真機過程比較漫長,使用這個直接在JVM上跑速度更快

常見的單元測試

本文不能把所有的情況都介紹到,也不能將所有的框架都介紹完全,挑選幾個主要的例子介紹來讓大家理解總體上的結構,然後自己挑選側重點根據文件深入的學習。

本地單元測試

Example:驗證一個數組的長度是否等於5,並且是否呼叫了add(1)這個操作,使用local test完成

public class ExampleUnitTest {

    @Test
    public void addition_isCorrect() throws Exception {
        List<Integer> mockList = Mockito.mock(List.class);
        mockList.add(1);
        Mockito.when(mockList.size()).thenReturn(5);
        Mockito.verify(mockList).add(1);
        assertEquals(5,mockList.size());
    }

}

首先我們知道一個問題,mockito所mock出來的物件都是假的,是的,沒錯,你用了一個假陣列,如果想要用真的,老老實實new一個,也就是說針對一個數組,所有的操作都是拿不到正常結果的,除非你想我上面那種方式去指定他。你如果執行mockList.size(),返回的是0,最後一句的作用是判斷兩個值是否相等。Mockito.verify是驗證函式,可以驗證這個方法是否被執行過。

Example:這次不用假陣列了,用真的!然後我們偷樑換柱,就是部分mock

public class Example2UnitTest {

    @Test
    public void spyText() {
        List<Integer> mockList = new ArrayList<>();
        mockList = Mockito.spy(mockList); //如果直接spy
        mockList.add(1);
        mockList.add(2);
        Mockito.when(mockList.size()).thenReturn(5);
        assertEquals(5, mockList.size());
        assertEquals(mockList.get(0) == 1, true);
        Mockito.when(mockList.size()).thenCallRealMethod();
        assertEquals(2, mockList.size());
    }

}

首先區分一下spy一個物件和spy一個類的區別:如果我此處呼叫Mockito.spy(List.class),那麼你得到的list仍然是假的,原因是看原始碼。

    @Incubating
    public static <T> T spy(Class<T> classToSpy) {
       return MOCKITO_CORE.mock(classToSpy, withSettings()
               .useConstructor()
               .defaultAnswer(CALLS_REAL_METHODS));
    }

很清晰,spy一個類是建立一個類的mock物件,然後呼叫一個mock類的真實返回,mock得到的類就是假的類,所以她的真實的返回值也是假的。所以我們此處spy一個物件。

spy一個物件是預設的物件的返回都使用真實資料返回,如果你進行修改,那麼就用你修改的類來做返回。這樣就能做到部分修改。我們還呼叫了這個Mockito.when(mockList.size()).thenCallRealMethod();這句的意思很清晰,就是使用這個函式的真實返回值,就算是一個mock出來的“假物件”,仍然可以用這個的到真實的返回值。

Android Instrumentation測試

Android Instrumentation測試跟本地測試區別不大,唯一的區別是執行的時候需要我們選擇一個裝置,然後gradle會打包測試用的Apk在手機上執行測試用例,如果想打log,換成Log.d等方法在Android Monitor中檢視

1.Example: 檢視當前的context是不是我的包名

@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
    @Test
    public void useAppContext() throws Exception {
        // Context of the app under test.
        Context appContext = InstrumentationRegistry.getTargetContext();
        assertEquals("com.ss.android.ies.mobcase.test", appContext.getPackageName());
    }
}

這是一個官方的例子,在androidTestCompile中,我們可以使用Api來發現一個當前的appContext,這在jvm中是做不到的。因為你沒有android環境。這裡判斷一下是否一樣。

2.Example: 使用espresso在UI上進行測試

androidTextCompile的優點是可以在android環境上進行測試,所以少不了UI的白盒測試,這裡舉一個例子,測試點選button之後editText的文字是否是123456

@RunWith(AndroidJUnit4.class)
public class TestActivityTest {

    @Rule
    public ActivityTestRule<TestActivity> activityTestRule = new ActivityTestRule<>(TestActivity.class);

    @Test
    public void buttonTest() {
        Espresso.onView(withId(R.id.button)).perform(click());
        Espresso.onView(withId(R.id.edit))
                .check(matches(withText("123456")));
    }

}

此處很好理解,espresso的語法跟自然語言一樣,寫單元測試分為三個步驟

  • 執行
  • 檢查

很簡單的幾句語法就能對UI進行測試,這裡我們進行了對button進行定位,然後執行click操作。第二部進行檢查,因為我的邏輯是點選按鈕editText.setText(“123456”),所以這個單元測試是通過的。