Android自動化測試入門(四)單元測試

Chsmy發表於2020-03-23

單元測試一般分兩類:

  • 本地測試:執行在本地的計算機上,這些測試編譯之後可以直接執行在本地的Java虛擬機器上(JVM)。可以最大限度的縮短執行的時間。如果測試中用到了Android框架中的物件,那麼谷歌推薦使用Robolectric來模擬物件。
  • 插樁測試:在Android裝置或者模擬器上執行的測試,這些測試可以訪問插樁測試資訊,比如被測裝置的Context,使用此方法可以執行具有複雜Android依賴的單元測試。前兩篇中的Espresso 和 UI Automator就是這類測試,Espresso一般用來測試單個介面,UI Automator一般用來測試多介面互動。它們執行的比本地測試慢很多,所以谷歌建議最好是必須針對裝置測試的時候才使用。

本地單元測試在Android自動化測試中是比重最大的一環,主要針對某個類中的某個方法。谷歌建議在所有的測試中,單元測試要佔到70%的比重,為啥它就這麼重要呢?

  • 本地單元測試相比於前面幾篇中的UI測試執行效率高,前面的UI測試是需要執行在手機上的,所以想要執行測試就需要執行程式碼的編譯、打包、安裝、執行,這是非常耗時的,特別是工程很大的時候,執行一次可能需要很長的時間。如果我們只是改變了程式碼中的一個方法,使用單元測試可以快速驗證該方法的正確性。
  • 提高寫程式碼的抽象和封裝能力,比如剛入行的時候,我們可能在一個按鈕的OnClickListener方法中寫一大坨程式碼,如果瞭解單元測試就會知道這樣寫對測試非常不友好,把這一坨提取封裝會更利於測試,也就能更快的驗證程式碼的正確性。
  • 因為單元測試是獨立的單個方法的測試,那麼當測試結果與預期不一致的時候,可以迅速定位bug。
  • 提高程式碼的穩定性,和易維護性,寫程式碼的時候能確保正確開發,在修改程式碼之後,保證功能不被破壞,其實編寫單元測試的過程也是對代自己寫的程式碼的Code Review,是對程式碼持續重構的開始。

本部分會用到四個小東西,Junit,Mockito,PowerMockito,Robolectric。Junit是單元測試框架,Mockito和Robolectric都是用來產生模擬物件的,Mockito在Java中用的多,PowerMockito是Mockito的增強版可以模擬final,static,private等Mockito不能mock的方法,Robolectric可以模擬更多的Andorid框架中的物件。

  • 如果要構建的本地單元測試對Android框架依賴小,可以選擇mockito,速度更快。
  • 如果要構建的本地單元測試對Android框架有很大的依賴性,可以選擇Robolectric

Junit

Junit是java中非常有名的測試框架,讓測試變得很容易。假如下面我們有一個toNumber的方法要測試

public class Utils {

    public Integer toNumber(String num){
        if(num == null || num.isEmpty()){
             return null;
        }
        Integer integer;
        try {
            integer = Integer.parseInt(num.trim());
        }catch (Exception e){
            integer = null;
        }
        return integer;
    }
    
}
複製程式碼

為了保證測試的全面性,我們可能需要設計下面的幾個測試用例

  • 如果傳入的是null,那麼應該返回null
  • 如果傳入的全是數字比如"12321",那麼應該返回整數12321
  • 如果傳入的字串左邊或者右邊,或者兩邊都有空格比如"123 "," 123"," 123 ",那麼應該返回正確的整數123
  • 如果傳入的字串中間有空格,或者有字母比如""12 3","12ab",這時候會發生崩潰,我們不讓他崩潰,讓他返回null

測試程式碼如下

public class ExampleUnitTest {

    @Test
    public void testToNumber_NotNullOrEmpty(){
        Utils utils = new Utils();
       assertNull(utils.toNumber(null));
       assertNull(utils.toNumber(""));
    }
    @Test
    public void testToNumber_hasSpace(){
        Utils utils = new Utils();
        assertEquals(new Integer("123"),utils.toNumber("123"));
        assertEquals(new Integer("123"),utils.toNumber("123 "));
        assertEquals(new Integer("123"),utils.toNumber(" 123 "));
    }
    @Test
    public void testToNumber_hasMiddleSpace(){
        Utils utils = new Utils();
        assertNull(utils.toNumber("12 3"));
        assertNull(utils.toNumber("12a3"));
    }
}
複製程式碼

其實寫單元測試也是對自己程式碼的一次檢查和重構,比如上面的toNumber方法,第一次寫的時候可能有很多問題都沒有想到直接返回一個Integer.parseInt()就完事了,隨著單元測試寫完並且測試用例都通過之後,這個方法也會變的更加健壯,變成了前面程式碼中所寫的那樣。

mockito

Junit已經能完成單元測試了,為啥要使用Mockito或者Robolectric?

我們需要明確單元測試的目的:單元測試的目的是為了測試我們自己寫的程式碼的正確性,它不需要測試外部的各種依賴,所以當我們遇到一個方法中有很多別的物件的依賴的時候,比如運算元據庫,連線網路,讀寫檔案等等,需要給它解依賴。

怎麼解依賴呢?其實就是弄一些假物件,比如程式碼中是我們從網路獲取一段json資料,轉化成一個物件傳入到我們的測試方法中。那麼就可以直接new一個假的物件,並給它設定我們期望的返回值傳給要測試的方法就好了,不需要再去請求網路獲取資料。這個過程稱之為mock

直接手動去new一個物件,然後去設定各種資料是比較麻煩的,而Mockito這類的框架就是用來簡化我們手動mock的。使用他們來建立一個虛擬物件設定返回值等操作會變得非常簡單。

下面開始練習,測試程式碼寫在 src/main/test/java資料夾下面

先練習使用mockito,引入依賴庫

testImplementation 'org.mockito:mockito-core:3.0.0'
複製程式碼

新建一個MockitoTest類,在類上新增註解@RunWith(MockitoJUnitRunner.class)表示Junit要把測試方法執行在MockitoJUnitRunner上

@RunWith(MockitoJUnitRunner.class)
public class MockitoTest {......}
複製程式碼

例子1: 結果驗證,測試某些結果是否正確,使用when和thenReturn表示當呼叫某個方法的時候指定返回值。最後通過assertEquals判斷返回值是否正確

@Test
    public void testMockitoResult() {
        Person person = mock(Person.class);
        //當呼叫person.getAge()方法的時候,給它返回一個18
        when(person.getAge()).thenReturn(18);
        //當呼叫person.getName()方法的時候,給它返回一個Lily
        when(person.getName()).thenReturn("Lily");
        //判斷返回跟預期是否一樣
        assertEquals(18, person.getAge());
        assertEquals("Lily", person.getName());
    }
複製程式碼

例子2: 驗證行為,有時候會測試某些行為是否被執行過,通過verify方法可以驗證某個方法是否執行過,執行的次數

 @Test
    public void testMockitoBehavior() {
        Person person = mock(Person.class);
        int age = person.getAge();
        //驗證getAge動作有沒有發生
        verify(person).getAge();
        //驗證person.getName()是不是沒有呼叫
        verify(person, never()).getName();
        //驗證是否最少呼叫過一次person.getAge
        verify(person, atLeast(1)).getAge();
        //驗證getAge動作是否被呼叫了2次,前面只用了一次所以這裡會報錯
        verify(person, times(2)).getAge();
    }
複製程式碼

例子3: 通過Mockito mock一個Person物件,那麼這個物件的name屬性是預設為null的,如果我們不想讓它為null,預設為空字串可以使用RETURNS_SMART_NULLS

   @Test
    public void testNotNull(){
        Person person = mock(Person.class);
        System.out.println(person.getName());
        Person person1 = mock(Person.class,RETURNS_SMART_NULLS);
        System.out.println(person1.getName());
    }
複製程式碼

例子4: 可以使用@Mock註解來mock一個物件比如

 @Mock
    List<Integer> mList;
    @Test
    public void testAnnotationMock(){
        mList.add(0);
        verify(mList).add(0);
    }
複製程式碼

例子5: 可以驗證是否執行了某個引數的方法

@Test
    public void testParameter(){
        Person person = mock(Person.class);
        when(person.getDuty(1)).thenReturn("醫生");
        System.out.println(person.getDuty(1));
        //anyInt任何Int值,此外還有anyString,anyFloat等
        when(person.getDuty(anyInt())).thenReturn("護士");
        System.out.println(person.getDuty(anyInt()));
        //驗證person.getDuty(1)方法有沒有呼叫
        verify(person).getDuty(ArgumentMatchers.eq(1));
    }
複製程式碼

例子6: mock出來的物件都是虛擬的物件,我們可以驗證其執行次數,狀態等,如果一個物件是真實的,那怎麼驗證呢 可以使用spy包裝一下

spy物件的方法預設呼叫真實的邏輯,mock物件的方法預設什麼都不做,或直接返回預設值。

  @Test
    public void testSpy(){
        Person person = getPerson();
        Person spy = spy(person);
        when(spy.getName()).thenReturn("Lily");
        System.out.println(spy.getName());
        verify(spy).getName();
    }
    private Person getPerson(){
        return new Person();
    }
複製程式碼

Mockito雖然好用但是也有些不足,比如不能mock static、final、private等物件,使用PowerMock就可以實現了

powermock

powermock官網

首先新增依賴

  testImplementation 'org.powermock:powermock-module-junit4:2.0.2'
  testImplementation 'org.powermock:powermock-api-mockito2:2.0.2'
複製程式碼

建立一個PowerMockTest類,在類上新增註解@RunWith(PowerMockRunner.class),通知Junit該類的測試方法執行在PowerMockRunner中。在新增註解@PrepareForTest(Utils.class)表示要測試的方法所在的類,這裡是一個自定義的Utils.class

例子1: 測試static方法

目標方法

 public static boolean isEmpty(@Nullable CharSequence str) {
        return str == null || str.length() == 0;
    }
複製程式碼

測試方法

  @Test
    public void testStatic(){
        PowerMockito.mockStatic(Utils.class);
        PowerMockito.when(Utils.isEmpty("abc")).thenReturn(false);
        assertFalse(Utils.isEmpty("abc"));
    }
複製程式碼

例子2: 測試private方法 替換私有變數

目標方法

 private String name;
 
 private String changeName(String name) {
        return "ABC" + name;
    }
 public String getName() {
        return name;
    }
複製程式碼

測試方法

  @Test
    public void testPrivate() throws Exception {
        Utils util = new Utils();
        //呼叫私有方法
        String res = Whitebox.invokeMethod(util, "changeName", "Lily");
        assertEquals("ABCLily",res);
        //替換私有變數  也可以使用MemberModifier來修改
        Whitebox.setInternalState(util,"name","Lily");
        assertEquals("Lily",util.getName());
    }
複製程式碼

例子3: 測試mock new關鍵字

目標方法

 public String getPersonName() {
        Person person = new Person("Lily");
        return person.getName();
    }
複製程式碼

測試方法

 @Test
    public void testNew() throws Exception {
        Person person = PowerMockito.mock(Person.class);
        Utils util = new Utils();
        //當new一個Person物件並傳入Lily的時候,返回person
        PowerMockito.whenNew(Person.class).withArguments("Lily").thenReturn(person);
        PowerMockito.when(util.getPersonName()).thenReturn("Diavd");
        assertEquals("Diavd",util.getPersonName());
    }
複製程式碼

目標方法getPersonName中new了一個Person,直接呼叫getPersonName方法會報錯,所以我們自己建立一個Person,並指定當當new一個Person物件並傳入Lily的時候,返回當前建立的person物件。然後在呼叫getPersonName方法就不會報錯了。

Robolectric

前面測試的類和依賴都是原生Java程式碼,可以直接執行在JVM上,當我們測試Android的時候,需要依賴Android SDK中的android.jar包,android.jar底層沒有具體的程式碼實現,因為它執行在Andorid系統中,Android系統中有預設的實現。

Mockito和PowerMockito都直接執行在JVM上,JVM上沒有Android原始碼相關的實現,那麼在做有Adroid相關的依賴的測試的時候,就會報錯,這時候就要用到Robolectric啦,當我們去呼叫android相關的程式碼的時候,它會攔截並去執行自己對相關程式碼的實現。

Robolectric官網

新增依賴

testImplementation 'androidx.test:core:1.2.0'
testImplementation 'org.robolectric:robolectric:4.3.1'
複製程式碼

Robolectric 4.0以上需要Android Gradle外掛/ Android Studio 3.2或更高版本。

在build.gradle中的android閉包下面新增下面程式碼,目前版本最高支援andorid sdk 28

android {
    compileSdkVersion 28
    testOptions.unitTests.includeAndroidResources = true
}
複製程式碼

在gradle.properties檔案中新增下面程式碼

android.enableUnitTestBinaryResources=true
複製程式碼

第一次執行的時候會下載相關jar包,網速不好可能要等很久

首先建立一個測試類RobolectricTest,新增註解@RunWith(RobolectricTestRunner.class)通知Junit框架該類中的測試方法執行在RobolectricTestRunner中。

@RunWith(RobolectricTestRunner.class)
public class RobolectricTest {...}
複製程式碼

例子1: 點選button,改變TextView上的文字,判斷改變之後的文字是不是預期的

    @Test
    public void clickingButtonShouldChangeMessage() {
        //預設會呼叫Activity的onCreate()、onStart()、onResume()
//        MainActivity activity = Robolectric.setupActivity(MainActivity.class);
//        TextView textView = activity.findViewById(R.id.tv_text);
//        Button button = activity.findViewById(R.id.btn_click);
//        button.performClick();
//        assertThat(textView.getText().toString(), equalTo("Hello Espresso!"));

        //Robolectric.setupActivity顯示過時了,使用ActivityScenario來代替
        //ActivityScenario提供api來啟動和驅動Activity的生命週期狀態以進行測試,
        // 適用於任意Activity,並能在不同版本的Android上一致工作
        //通過scenario.moveToState來控制生命週期比如  scenario.moveToState(Lifecycle.State.CREATED)
        ActivityScenario<MainActivity> scenario = ActivityScenario.launch(MainActivity.class);
        scenario.onActivity(activity -> {
            TextView textView = activity.findViewById(R.id.tv_text);
            Button button = activity.findViewById(R.id.btn_click);
            button.performClick();
            assertThat(textView.getText().toString(), equalTo("Hello Espresso!"));
        });
    }
複製程式碼

使用Robolectric.setupActivity可以啟動一個Activity,不過使用的時候顯示該方法已過期,最新的可以使用ActivityScenario來啟動一個Activity

ActivityScenario提供api來啟動和驅動Activity的生命週期狀態以進行測試,適用於任意Activity,並能在不同版本的Android上一致工作,通過scenario.moveToState來控制生命週期比如 scenario.moveToState(Lifecycle.State.CREATED)

例子2: 點選按鈕從MainActivity到UnitTestActivity,Robolectric是執行在JVM上的測試框架,並不會真正的啟動UnitTestActivity,但是可以檢查MainActivity是不是觸發了真正的意圖

    //Application用的比較多,可以初始換一個全域性的
    private Application context;

    @Before
    public void setUp() throws Exception {
        context = ApplicationProvider.getApplicationContext();
    }
  @Test
    public void testClickButtonToPicking() {
        ActivityScenario<MainActivity> scenario = ActivityScenario.launch(MainActivity.class);
        scenario.onActivity(activity -> {
            Button button = activity.findViewById(R.id.btn_go_to_unit);
            button.performClick();
            //期望的intent
            Intent expectedIntent = new Intent(activity, UnitTestActivity.class);
            //真實的intent
            Intent actual = shadowOf(context)
                    .getNextStartedActivity();
            assertEquals(expectedIntent.getComponent(),actual.getComponent());
        });
    }
複製程式碼

例子3: Shadow是Robolectric的核心,Robolectric中內建了很多Android SDK中的類的影子,比如ShadowCompoundButton,ShadowTextView,ShadowActivity .....

當一個android.jar中的某個類被呼叫的時候,Robolectric會嘗試尋找該類的影子,呼叫影子中的方法,通過shadowOf可以很方便的拿到對應類的影子類

測試Toast顯示

  @Test
    public void testToast(){
        ActivityScenario<MainActivity> scenario = ActivityScenario.launch(MainActivity.class);
        scenario.onActivity(activity -> {
            Button button = activity.findViewById(R.id.btn_show_toast);
            button.performClick();

            Toast latestToast = ShadowToast.getLatestToast();
            assertNotNull(latestToast);
            assertEquals("測試Toast", ShadowToast.getTextOfLatestToast());
        });
    }
複製程式碼

更多例子可檢視原始碼 Robolectric

本篇對本地單元測試的一些常用的庫做了一些練習,練習完成就算是入門了,之後寫單元測試哪裡不熟悉就直接去查文件了。而通過本篇練習本篇最主要的收穫就是,以後寫程式碼的時候要時刻有測試意識,盡最大努力寫出可測試易維護的程式碼

參考:

Android 官網測試文件

Android單元測試與模擬測試

使用強大的 Mockito 來測試你的程式碼

Android單元測試(一)

Android單元測試(二)

Mockito與PowerMock的使用基礎教程

Mockito教程

一文全面瞭解Android單元測試

相關文章