Android自動化測試入門(四)單元測試
Android自動化測試入門(四)單元測試
單元測試一般分兩類:
- 本地測試:運行在本地的計算機上,這些測試編譯之後可以直接運行在本地的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
首先添加依賴
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相關的代碼的時候,它會攔截並去執行自己對相關代碼的實現。
添加依賴
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
本篇對本地單元測試的一些常用的庫做了一些練習,練習完成就算是入門了,之後寫單元測試哪裏不熟悉就直接去查文檔了。 而通過本篇練習本篇最主要的收穫就是,以後寫代碼的時候要時刻有測試意識,盡最大努力寫出可測試易維護的代碼 。
參考: