單元測試一般分兩類:
- 本地測試:執行在本地的計算機上,這些測試編譯之後可以直接執行在本地的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
本篇對本地單元測試的一些常用的庫做了一些練習,練習完成就算是入門了,之後寫單元測試哪裡不熟悉就直接去查文件了。而通過本篇練習本篇最主要的收穫就是,以後寫程式碼的時候要時刻有測試意識,盡最大努力寫出可測試易維護的程式碼。
參考: