Android单元测试学习总结
文章目錄
- 一、本地單元測試
- 1. 創建測試類
- 2. Assert類中的常用斷言方法
- 3. 運行測試類
- 4. 運行單個測試方法或多個測試類
- 二、Mockito測試框架的使用
- 1. Mock概念的理解
- 2. Mockito中幾種Mock對象的方式
- 3. 驗證行為
- verify(T mock)函數的使用
- 使用`when(T methodCall)`函數
- 使用`thenAnswer`為回調做測試樁
- 使用`doCallRealMethod()`函數來調用某個方法的真實實現方法
- 使用`doNothing()`函數是為了設置void函數什么也不做
- 使用`doAnswer()`函數測試void函數的回調
- 需要使用doReturn函數代替thenReturn的情況
- 使用`doThrow()`函數來測試void函數拋出異常
- 4. 驗證方法的調用次數
- 5. 參數匹配器 (matchers)
- 6. 使用InOrder驗證執行執行順序
- 7. 使用Spy監控真實對象
- 8. 使用ArgumentCaptor進行參數捕獲
- 9. 使用@InjectMocks自動注入依賴對象
- 三、PowerMockito框架使用
- 1. 普通Mock的方式
- 2. Mock方法內部new出來的對象
- 3. Mock普通對象的final方法
- 4. Mock普通類的靜態方法
- 5. verify靜態方法的調用次數
- 6. 使用真實返回值
- 7. Mock私有方法
- 8. Mock普通類的私有變量
- 9. 對靜態void方法進行Mock
- 10. Mock系統的final靜態類
- 四、Robolectric測試框架的使用
- 五、Espresso測試框架的使用
Android單元測試主要分為以下兩種
- 本地單元測試(Junit Test), 本地單元測試是純java代碼的測試,只運行在本地電腦的JVM環境上,不依賴于Android框架的任何api, 因此執行速度快,效率較高,但是無法測試Android相關的代碼。
- 儀器化測試(Android Test),是針對Android相關代碼的測試,需要運行在真機設備或模擬器上,運行速度較慢,但是可以測試UI的交互以及對設備信息的訪問,得到接近真實的測試結果。
在Android Studio中新建一個項目的時候,app的gradle中會默認添加單元測試的相關依賴庫:
dependencies {implementation fileTree(dir: 'libs', include: ['*.jar'])testImplementation 'junit:junit:4.12'androidTestImplementation 'com.android.support.test:runner:1.0.2'androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' }其中testImplementation添加的依賴就是本地化測試庫, androidTestImplementation 添加的依賴則是Android環境下的測試庫,同時,在項目的工程目錄下也會默認創建好測試的目錄:
其中app/src/test/下面存放的是Junit本地測試代碼,app/src/androidTest/下面存放的是Android測試代碼。
一、本地單元測試
進行本地單元測試需要先了解一些基本的Junit注解:
| @Test | 定義所在方法為單元測試方法,方法必須是public void |
| @Before | 定義所在方法在每個測試用例執行之前執行一次, 用于準備測試環境(如: 初始化類,讀輸入流等),在一個測試類中,每個@Test方法的執行都會觸發一次調用 |
| @After | 定義所在方法在每個測試用例執行之后執行一次,用于清理測試環境數據,在一個測試類中,每個@Test方法的執行都會觸發一次調用。 |
| @BeforeClass | 定義所在方法在測試類里的所有用例運行之前運行一次,方法必須是public static void,用于做一些耗時的初始化工作(如: 連接數據庫) |
| @AfterClass | 定義所在方法在測試類里的所有用例運行之后運行一次,方法必須是public static void,用于清理數據(如: 斷開數據連接) |
| @Test (expected = Exception.class) | 如果該測試方法沒有拋出Annotation中的Exception類型(子類也可以),則測試失敗 |
| @Test(timeout=100) | 如果該測試方法耗時超過100毫秒,則測試失敗,用于性能測試 |
| @Ignore 或者 @Ignore(“太耗時”) | 忽略當前測試方法,一般用于測試方法還沒有準備好,或者太耗時之類的 |
| @FixMethodOrder | 定義所在的測試類中的所有測試方法都按照固定的順序執行,可以指定3個值,分別是DEFAULT、JVM、NAME_ASCENDING(字母順序) |
| @RunWith | 指定測試類的測試運行器 |
更多可以參考Junit官網:https://junit.org/junit4/
1. 創建測試類
接下來就可以創建測試類,除了可以手動創建測試類外,可以利用AS快捷鍵:將光標選中要創建測試類的類名上->按下ALT + ENTER->在彈出的彈窗中選擇Create Test
這會彈出下面的彈窗,或者鼠標在類名上右鍵選擇菜單Go to–>Test,也會彈出下面的彈窗
勾選需要進行測試的方法,會自動生成一個測試類:
如果勾選了@Before或@After的話也會自動給你生成對應的測試方法
接下來編寫測試方法,首先在要測試的目標類中寫幾個業務方法:
public class SimpleClass {public boolean isTeenager(int age) {if (age < 15) {return true;}return false;}public int add(int a, int b) {return a + b;}public String getNameById(int id) {if (id == 1) {return "小明";} else if (id == 2){return "小紅";}return "";} }然后,測試類:
@RunWith(JUnit4.class) public class SimpleClassTest {private SimpleClass simpleClass;@Beforepublic void setUp() throws Exception {simpleClass = new SimpleClass();}@Afterpublic void tearDown() throws Exception {}@Testpublic void isTeenager() {Assert.assertFalse(simpleClass.isTeenager(20));Assert.assertTrue(simpleClass.isTeenager(14));}@Testpublic void add() {Assert.assertEquals(simpleClass.add(3, 2), 5);Assert.assertNotEquals(simpleClass.add(3, 2), 4);}@Testpublic void getNameById() {Assert.assertEquals(simpleClass.getNameById(1), "小明");Assert.assertEquals(simpleClass.getNameById(2), "小紅");Assert.assertEquals(simpleClass.getNameById(10), "");} }其中setUp()是自動生成的添加了@Before注解,這會在每個測試方法執行前執行,因此在這里創建一個目標對象,也可以選擇添加@BeforeClass注解但這時setUp()應該改為靜態的方法。然后在每個測試方法中編寫測試用例,這里使用org.junit.Assert包中的斷言方法,有很多assertXXX方法,可以自己選擇用來判斷目標方法的結果是否滿足預期。
2. Assert類中的常用斷言方法
| assertNull(Object object) | 斷言對象為空 |
| assertNull(String message, Object object) | 斷言對象為空,如果不為空拋出異常攜帶指定的message信息 |
| assertNotNull(Object object) | 斷言對象不為空 |
| assertNotNull(Object object) | 斷言對象不為空,如果為空拋出異常攜帶指定的message信息 |
| assertSame(Object expected, Object actual) | 斷言兩個對象引用的是同一個對象 |
| assertSame(String message, Object expected, Object actual) | 斷言兩個對象引用的是同一個對象,否則拋出異常攜帶指定的message信息 |
| assertNotSame(Object expected, Object actual) | 斷言兩個對象引用的不是同一個對象 |
| assertNotSame(String message, Object expected, Object actual) | 斷言兩個對象引用的不是同一個對象,否則拋出異常攜帶指定的message信息 |
| assertTrue(boolean condition) | 斷言結果為true |
| assertTrue(String message, boolean condition) | 斷言結果為true, 為false時拋出異常攜帶指定的message信息 |
| assertFalse(boolean condition) | 斷言結果為false |
| assertFalse(String message, boolean condition) | 斷言結果為false, 為true時拋出異常攜帶指定的message信息 |
| assertEquals(long expected, long actual) | 斷言兩個long 類型 expected 和 actual 的值相等 |
| assertEquals(String message, long expected, long actual) | 斷言兩個long 類型 expected 和 actual 的值相等,如不相等則拋異常攜帶指定message信息 |
| assertEquals(Object expected, Object actual) | 斷言兩個對象相等 |
| assertEquals(String message, Object expected, Object actual) | 斷言兩個對象相等,如果不相等則拋出異常攜帶指定的message信息 |
| assertEquals(float expected, float actual, float delta) | 斷言兩個 float 類型 expect 和 actual 在 delta 偏差值下相等,delta是誤差精度 |
| assertEquals(String message, float expected, float actual, float delta) | 斷言兩個 float 類型 expect 和 actual 在 delta 偏差值下相等,如果不相等則拋出異常攜帶指定的message信息 |
| assertEquals(double expected, double actual, double delta) | 斷言兩個 double 類型 expect 和 actual 在 delta 偏差值下相等 |
| assertEquals(String message, double expected,double actual, double delta) | 斷言兩個 double 類型 expect 和 actual 在 delta 偏差值下相等,如果不相等則拋出異常攜帶指定的message信息 |
| assertArrayEquals(T[] expected, T[] actual) | 斷言兩個相同類型的數組的元素一一對應相等 |
| assertArrayEquals(String message, T[] expected, T[] actual) | 斷言兩個相同類型的數組的元素一一對應相等,如果不相等則拋出異常攜帶指定的message信息 |
| fail() | 直接讓測試失敗 |
| fail(String message) | 直接讓測試失敗并給出message錯誤信息 |
| assertThat(T actual, Matcher<? super T> matcher) | 斷言actual和matcher規則匹配 |
| assertThat(String reason, T actual, Matcher<? super T> matcher) | 斷言actual和matcher規則匹配,否則拋出異常攜帶指定的reason信息 |
其中assertEquals的方法,都對應有一個assertNotEquals方法,這里不列了,assertThat是一個強大的方法:
Assert.assertThat(1, is(1));Assert.assertThat(0, is(not(1)));Assert.assertThat("hello", startsWith("h"));List<String> items = new ArrayList<>();items.add("aaa");items.add("bbb");Assert.assertThat(items, hasItem("aaa"));需要靜態導入org.hamcrest.Matchers類里面的方法,更多匹配方法請參考這個類。
3. 運行測試類
選中測試類右鍵Run運行,控制面板中就會顯示測試結果:
如果所有的測試用例都正常返回了預期的結果,則面板中左側每個測試方法前面會帶一個綠色的對勾,否則方法前面會變成紅色感嘆號并且控制面板會輸出異常,現在來改一個業務方法試一下:
這里將age < 15改為輸出false,假設這是我們在編碼的時候由于疏忽粗心造成的,然后運行測試類:
控制面板會告訴那一行出錯了:
也就是說這里沒有返回預期的結果,說明我們編寫的業務邏輯是有錯誤的,這時就需要改bug了。
4. 運行單個測試方法或多個測試類
上面是運行的整個測試類,如果要運行測試類的單個方法,則鼠標只選中某個要運行的測試方法,然后右鍵選擇Run即可。如果要同時運行多個測試類,而如果多個測試類在同一個包下面,則選中多個測試類所在的包目錄,然后右鍵選擇Run運行。否則可以通過下面的方式指定,創建一個空的測試類,然后添加注解:
@RunWith(Suite.class) @Suite.SuiteClasses({SimpleClassTest.class, SimpleClass2Test.class}) public class RunMultiTest { }運行這個測試類就可以將指定的測試類的方法一起運行。
二、Mockito測試框架的使用
前面介紹的只能測試不涉及Android相關Api的java代碼用例,如果涉及到Android相關Api的時候,就不方便了,這時如果不依賴第三方庫的話可能需要使用儀器化測試跑到Android設備上去運行,于是有一些比較好的第三方的替代框架可以來模擬使用Android的代碼測試,Mockito就是基于依賴注入實現的一個測試框架。
1. Mock概念的理解
什么是Mock, 這個單詞的中文意思就是“模仿”或者“虛假”的意思,也就是要模仿一個對象,為啥要模仿?
在傳統的JUnit單元測試中,沒有消除在測試中對對象的依賴,如A對象依賴B對象方法,在測試A對象的時候,我們需要構造出B對象,這樣子增加了測試的難度,或者使得我們對某些類的測試無法實現。這與單元測試的思路相違背。
還有一個主要的問題就是本地單元測試由于是運行本地JVM環境,無法依賴Android的api,只靠純Junit的測試環境很難模擬出完整的Android環境,導致無法測試Android相關的代碼,而Mock就能解決這個問題,通過Mock能夠很輕易的實現對象的模擬。
添加依賴:
dependencies {implementation fileTree(dir: 'libs', include: ['*.jar'])testImplementation 'org.mockito:mockito-core:2.19.0'.... }2. Mockito中幾種Mock對象的方式
使用之前通過靜態方式導入會使用更方便:
// 靜態導入會使代碼更簡潔import static org.mockito.Mockito.*;直接mock一個對象:
@Testpublic void testMock() {SimpleClass mockSimple = Mockito.mock(SimpleClass.class);assertNotNull(mockSimple);}注解方式mock一個對象:
@MockSimpleClass simple;@Beforepublic void setUp() {MockitoAnnotations.initMocks(this);}@Testpublic void testMock() {assertNotNull(simple);}運行器方式mock一個對象:
@RunWith(MockitoJUnitRunner.class) public class ExampleUnitTest {@MockSimpleClass simple;@Testpublic void testMock() {assertNotNull(simple);} }MockitoRule方式mock一個對象:
public class ExampleUnitTest {@MockSimpleClass simple;@Rule //<--使用@Rulepublic MockitoRule mockitoRule = MockitoJUnit.rule();@Testpublic void testMock() {assertNotNull(simple);} }3. 驗證行為
verify(T mock)函數的使用
verify(T mock)的作用是驗證發生的某些行為等同于verify(mock, times(1)) 例如:
@Test public void testMock() {//創建mock對象List mockedList = mock(List.class);//使用mock對象mockedList.add("one");mockedList.clear();//驗證mockedList.add("one")是否被調用,如果被調用則當前測試方法通過,否則失敗verify(mockedList).add("one");//驗證 mockedList.clear()是否被調用,如果被調用則當前測試方法通過,否則失敗verify(mockedList).clear();} @Test public void testMock() {mock.someMethod("some arg");//驗證mock.someMethod("some arg")是否被調用,如果被調用則測試方法通過,否則失敗verify(mock).someMethod("some arg");}也就是說如果把調用的方法注釋掉,則運行testMock()方法就會失敗。
通過verify關鍵字,一旦mock對象被創建了,mock對象會記住所有的交互。然后你就可能選擇性的驗證你感興趣的交互。
通常需要配合一些測試方法來驗證某些行為,這些方法稱為"打樁方法"(Stub),打樁的意思是針對mock出來的對象進行一些模擬操作,如設置模擬的返回值或拋出異常等。
常見的打樁方法:
| doReturn(Object toBeReturned) | 提前設置要返回的值 |
| doThrow(Throwable… toBeThrown) | 提前設置要拋出的異常 |
| doAnswer(Answer answer) | 提前對結果進行攔截 |
| doCallRealMethod() | 調用某一個方法的真實實現 |
| doNothing() | 設置void函數什么也不做 |
| thenReturn(T value) | 設置要返回的值 |
| thenThrow(Throwable… throwables) | 設置要拋出的異常 |
| thenAnswer(Answer<?> answer) | 對結果進行攔截 |
例如:
@Testpublic void testMock() {// 你可以mock具體的類型,不僅只是接口List mockedList = mock(List.class);// 打測試樁when(mockedList.get(0)).thenReturn("first");doReturn("aaaa").when(mockedList).get(1);when(mockedList.get(1)).thenThrow(new RuntimeException());doThrow(new RuntimeException()).when(mockedList).clear();// 輸出“first”System.out.println(mockedList.get(0));// 因為get(999) 沒有打樁,因此輸出null, 注意模擬環境下這個地方是不會報IndexOutOfBoundsException異常的System.out.println(mockedList.get(999));// get(1)時會拋出異常System.out.println(mockedList.get(1));// clear會拋出異常mockedList.clear();}doXXX和thenXXX使用上差不多,一個是調用方法之前設置好返回值,一個是在調用方法之后設置返回值。默認情況下,Mock出的對象的所有非void函數都有返回值,對象類型的默認返回的是null,例如返回int、boolean、String的函數,默認返回值分別是0、false和null。
使用when(T methodCall)函數
打樁方法需要配合when(T methodCall)函數,意思是使測試樁方法生效。當你想讓這個mock能調用特定的方法返回特定的值,那么你就可以使用它。
例如:
when(mock.someMethod()).thenReturn(10);//你可以使用靈活的參數匹配,例如 when(mock.someMethod(anyString())).thenReturn(10);//設置拋出的異常when(mock.someMethod("some arg")).thenThrow(new RuntimeException());//你可以對不同作用的連續回調的方法打測試樁://最后面的測試樁(例如:返回一個對象:"foo")決定了接下來的回調方法以及它的行為。when(mock.someMethod("some arg")).thenReturn("foo")//第一次調用someMethod("some arg")會返回"foo".thenThrow(new RuntimeException());//第二次調用someMethod("some arg")會拋異常//可以用以下方式替代比較小版本的連貫測試樁:when(mock.someMethod("some arg")).thenReturn("one", "two");//和下面的方式效果是一樣的when(mock.someMethod("some arg")).thenReturn("one").thenReturn("two");//比較小版本的連貫測試樁并且拋出異常:when(mock.someMethod("some arg")).thenThrow(new RuntimeException(), new NullPointerException();使用thenAnswer為回調做測試樁
when(mock.someMethod(anyString())).thenAnswer(new Answer() {Object answer(InvocationOnMock invocation) {Object[] args = invocation.getArguments();Object mock = invocation.getMock();return "called with arguments: " + args;}});// 輸出 : "called with arguments: foo"System.out.println(mock.someMethod("foo"));使用doCallRealMethod()函數來調用某個方法的真實實現方法
注意,在Mock環境下,所有的對象都是模擬出來的,而方法的結果也是需要模擬出來的,如果你沒有為mock出的對象設置模擬結果,則會返回默認值,例如:
public class Person {public String getName() {return "小明";} }@Test public void testPerson() {Person mock = mock(Person.class);//輸出null,除非設置發回模擬值when(mock.getName()).thenReturn("xxx");System.out.println(mock.getName()); }因為getName()方法沒有設置模擬返回值,而getName()返回值是String類型的,因此直接調用的話會返回String的默認值null,所以上面代碼如果要想輸出getName()方法的真實返回值的話,需要設置doCallRealMethod():
@Testpublic void testPerson() {Person mock = mock(Person.class);doCallRealMethod().when(mock).getName();//輸出“小明”System.out.println(mock.getName());}使用doNothing()函數是為了設置void函數什么也不做
需要注意的是默認情況下返回值為void的函數在mocks中是什么也不做的但是,也會有一些特殊情況。如:
測試樁連續調用一個void函數時:
doNothing().doThrow(new RuntimeException()).when(mock).someVoidMethod();//does nothing the first time:mock.someVoidMethod();//throws RuntimeException the next time:mock.someVoidMethod();監控真實的對象并且你想讓void函數什么也不做:
List list = new LinkedList(); List spy = spy(list);//let's make clear() do nothing doNothing().when(spy).clear();spy.add("one");//clear() does nothing, so the list still contains "one" spy.clear();使用doAnswer()函數測試void函數的回調
當你想要測試一個無返回值的函數時,可以使用一個含有泛型類Answer參數的doAnswer()函數做回調測試。假設你有一個void方法有多個回調參數,當你想指定執行某個回調時,使用thenAnswer很難實現了,如果使用doAnswer()將非常簡單,示例代碼如下:
MyCallback callback = mock(MyCallback.class); Mockito.doAnswer(new Answer() {@Overridepublic Object answer(InvocationOnMock invocationOnMock) throws Throwable {//獲取第一個參數MyCallback call = invocation.getArgument(0);//指定回調執行操作call.onSuccess();return null;}}).when(mockedObject.requset(callback));doAnswer(new Answer() {@Overridepublic Object answer(InvocationOnMock invocation) throws Throwable {System.out.println("onSuccess answer");return null;}}).when(callback).onSuccess();mockedObject.requset(callback)需要使用doReturn函數代替thenReturn的情況
如當監控真實的對象并且調用真實的函數帶來的影響時
List list = new LinkedList(); List spy = spy(list);//不可能完成的:真實方法被調用的時候list仍是空的,所以spy.get(0)會拋出IndexOutOfBoundsException()異常 when(spy.get(0)).thenReturn("foo");//這時你應該使用doReturn()函數 doReturn("foo").when(spy).get(0);使用doThrow()函數來測試void函數拋出異常
SimpleClass mock = mock(SimpleClass.class); doThrow(new RuntimeException()).when(mock).someVoidMethod(); mock.someVoidMethod();總之使用doThrow(), doAnswer(), doNothing(), doReturn() and doCallRealMethod() 這些函數時可以在適當的情況下調用when()來解決一些問題., 如當你需要下面這些功能時這是必須的:
- 測試void函數
- 在受監控的對象上測試函數
- 不只一次的測試同一個函數,在測試過程中改變mock對象的行為
4. 驗證方法的調用次數
需要配合使用一些方法
| times(int wantedNumberOfInvocations) | 驗證調用方法的次數 |
| never() | 驗證交互沒有發生,相當于times(0) |
| only() | 驗證方法只被調用一次,相當于times(1) |
| atLeast(int minNumberOfInvocations) | 至少進行n次驗證 |
| atMost(int maxNumberOfInvocations) | 至多進行n次驗證 |
| after(long millis) | 在給定的時間后進行驗證 |
| timeout(long millis) | 驗證方法執行是否超時 |
| description(String description) | 驗證失敗時輸出的內容 |
| verifyZeroInteractions | 驗證mock對象沒有交互 |
例如:
mock.someMethod("some arg"); mock.someMethod("some arg"); //驗證mock.someMethod("some arg")被連續調用兩次,即如果沒有調用兩次則驗證失敗 verify(mock, times(2)).someMethod("some arg"); //注意,下面三種是等價的,都是驗證someMethod()被只調用一次 verify(mock).someMethod("some arg"); verify(mock, times(1)).someMethod("some arg"); verify(mock, only()).someMethod("some arg"); mPerson.getAge(); mPerson.getAge(); //驗證至少調用2次 verify(mPerson, atLeast(2)).getAge(); //驗證至多調用2次 verify(mPerson, atMost(2)).getAge(); //下面兩種等價,驗證調用次數為0 verify(mPerson, never()).getAge(); verify(mPerson, times(0)).getAge(); mPerson.getAge(); mPerson.getAge(); long current = System.currentTimeMillis(); System.out.println(current ); //延時1s后驗證mPerson.getAge()是否被執行了2次 verify(mPerson, after(1000).times(2)).getAge(); System.out.println(System.currentTimeMillis() - current); mPerson.getAge();mPerson.getAge();//驗證方法在100ms超時前被調用2次verify(mPerson, timeout(100).times(2)).getAge(); @Testpublic void testVerifyZeroInteractions() {Person person = mock(Person.class);person.eat("a");//由于person對象發生了交互,所以這里驗證失敗,把上面的調用注釋掉這里就會驗證成功verifyZeroInteractions(person);//可以驗證多個對象沒有交互//verifyZeroInteractions(person,person2 );} @Testpublic void testVerifyZeroInteractions() {Person person = mock(Person.class);person.eat("a");verify(person).eat("a");//注意,這將會無法到達驗證目的,不能跟verify()混用verifyZeroInteractions(person,person2 );}5. 參數匹配器 (matchers)
Mockito以自然的java風格來驗證參數值: 使用equals()函數。有時,當需要額外的靈活性時你可能需要使用參數匹配器,也就是argument matchers :
// 使用內置的anyInt()參數匹配器when(mockedList.get(anyInt())).thenReturn("element");// 使用自定義的參數匹配器( 在isValid()函數中返回你自己的匹配器實現 )when(mockedList.contains(argThat(isValid()))).thenReturn("element");// 輸出elementSystem.out.println(mockedList.get(999));// 你也可以驗證參數匹配器verify(mockedList).get(anyInt());常用的參數匹配器:
| anyObject() | 匹配任何對象 |
| any(Class type) | 與anyObject()一樣 |
| any() | 與anyObject()一樣 |
| anyBoolean() | 匹配任何boolean和非空Boolean |
| anyByte() | 匹配任何byte和非空Byte |
| anyCollection() | 匹配任何非空Collection |
| anyDouble() | 匹配任何double和非空Double |
| anyFloat() | 匹配任何float和非空Float |
| anyInt() | 匹配任何int和非空Integer |
| anyList() | 匹配任何非空List |
| anyLong() | 匹配任何long和非空Long |
| anyMap() | 匹配任何非空Map |
| anyString() | 匹配任何非空String |
| contains(String substring) | 參數包含給定的substring字符串 |
| argThat(ArgumentMatcher matcher) | 創建自定義的參數匹配模式 |
| eq(T value) | 匹配參數等于某個值 |
一些示例代碼:
@Testpublic void testPersonAny(){when(mPerson.eat(any(String.class))).thenReturn("米飯");//或:when(mPerson.eat(anyString())).thenReturn("米飯");//輸出米飯System.out.println(mPerson.eat("面條"));}@Testpublic void testPersonContains(){when(mPerson.eat(contains("面"))).thenReturn("面條");//輸出面條System.out.println(mPerson.eat("面"));}@Testpublic void testPersonArgThat(){//自定義輸入字符長度為偶數時,輸出面條。when(mPerson.eat(argThat(new ArgumentMatcher<String>() {@Overridepublic boolean matches(String argument) {return argument.length() % 2 == 0;}}))).thenReturn("面條");//輸出面條System.out.println(mPerson.eat("1234"));}需要注意的是,如果你打算使用參數匹配器,那么所有參數都必須由匹配器提供。例如:
verify(mock).someMethod(anyInt(), anyString(), eq("third argument")); // 上述代碼是正確的,因為eq()也是一個參數匹配器verify(mock).someMethod(anyInt(), anyString(), "third argument"); // 上述代碼是錯誤的, 因為所有參數必須由匹配器提供,而參數"third argument"并非由參數匹配器提供,因此會拋出異常像anyObject(), eq()這樣的匹配器函數不會返回匹配器。它們會在內部將匹配器記錄到一個棧當中,并且返回一個假的值,通常為null。
6. 使用InOrder驗證執行執行順序
驗證執行執行順序主要使用InOrder函數
如,驗證mock一個對象的函數執行順序:
驗證多個mock對象的函數執行順序:
@Testpublic void testInorderMulti() {List<String> firstMock = mock(List.class);List<String> secondMock = mock(List.class);firstMock.add("小明");secondMock.add("小紅");// 為這兩個Mock對象創建inOrder對象InOrder inOrder = inOrder(firstMock, secondMock);// 驗證它們的執行順序inOrder.verify(firstMock).add("小明");inOrder.verify(secondMock).add("小紅");}驗證執行順序是非常靈活的,你不需要一個一個的驗證所有交互,只需要驗證你感興趣的對象即可。 你可以選擇單個mock對象和多個mock對象混合著來,也可以僅通過那些需要驗證順序的mock對象來創建InOrder對象。
7. 使用Spy監控真實對象
監控真實對象使用spy()函數生成,或者也可以像@Mock那樣使用@Spy注解來生成一個監控對象, 當你你為真實對象創建一個監控(spy)對象后,在你使用這個spy對象時真實的對象也會也調用,除非它的函數被stub了。盡量少使用spy對象,使用時也需要小心形式。
@Testpublic void testSpy() {List<String> list = new ArrayList<>();List<String> spy = spy(list);// 你可以選擇為某些函數打樁when(spy.size()).thenReturn(100);// 調用真實對象的函數spy.add("one");spy.add("two");// 輸出第一個元素"one"System.out.println(spy.get(0));// 因為size()函數被打樁了,因此這里返回的是100System.out.println(spy.size());// 驗證交互verify(spy).add("one");verify(spy).add("two");}使用@Spy生成監控對象:
@SpyPerson mSpyPerson;@Testpublic void testSpyPerson() {//將會輸出Person 類中getName()的真實實現,而不是nullSystem.out.println(mSpyPerson.getName());}理解監控真實對象非常重要!有時,在監控對象上使用when(Object)來進行打樁是不可能或者不切實際的。因此,當使用監控對象時請考慮doReturn|Answer|Throw()函數族來進行打樁。例如:
List list = new LinkedList(); List spy = spy(list);// 不可能實現 : 因為當調用spy.get(0)時會調用真實對象的get(0)函數, // 此時會發生IndexOutOfBoundsException異常,因為真實List對象是空的when(spy.get(0)).thenReturn("foo");// 你需要使用doReturn()來打樁 doReturn("foo").when(spy).get(0);8. 使用ArgumentCaptor進行參數捕獲
參數捕獲主要為了下一步的斷言做準備,示例代碼:
@Testpublic void argumentCaptorTest() {List<Object> mock = mock(List.class);mock.add("John");//構建要捕獲的參數類型,這里是StringArgumentCaptor argument = ArgumentCaptor.forClass(String.class);//在verify方法的參數中調用argument.capture()方法來捕獲輸入的參數verify(mock).add(argument.capture());//驗證“John”參數捕獲assertEquals("John", argument.getValue());} @Testpublic void argumentCaptorTest2() {List<Object> mock = mock(List.class);mock.add("Brian");mock.add("Jim");ArgumentCaptor argument = ArgumentCaptor.forClass(String.class);verify(mock, times(2)).add(argument.capture());//如果又多次參數調用,argument.getValue()捕獲到的是最后一次調用的參數assertEquals("Jim", argument.getValue());//如果要獲取所有的參數值可以調用argument.getAllValues()assertArrayEquals(new Object[]{"Brian","Jim"}, argument.getAllValues().toArray());}9. 使用@InjectMocks自動注入依賴對象
有時我們要測試的對象內部需要依賴另一個對象,例如:
public class User {private Address address;public void setAddress(Address address) {this.address = address;}public String getAddress() {return address.getDetail();} } public class Address {public String getDetail() {return "detail Address";} }User類內部需要依賴Address類,當我們測試的時候需要mock出這兩個對象,然后將Address對象傳入到User當中,這樣如果依賴的對象多了的話就相當麻煩,Mockito 提供了可以不用去手動注入對象的方法,首先使用@InjectMocks注解需要被注入的對象,如User,然后需要被依賴注入的對象使用@Mock或@Spy注解,之后Mockito 會自動完成注入過程,例如:
@InjectMocksUser mTestUser;@MockAddress mAddress;@Testpublic void argumentInjectMock() {when(mAddress.getDetail()).thenReturn("浙江杭州");System.out.println(mTestUser.getAddress());}這樣就不用關心為User 設置Address ,只要為User需要依賴的類添加注解就可以了,然后直接將重點放到測試方法的編寫上。
或者使用@Spy監控真實對象注入也可以:
@InjectMocksUser mTestUser;@SpyAddress mAddress;@Testpublic void argumentInjectMock() {// when(mAddress.getDetail()).thenReturn("浙江杭州");System.out.println(mTestUser.getAddress());}其他:
連續調用的另一種更簡短的版本:
// 第一次調用時返回"one",第二次返回"two",第三次返回"three"when(mock.someMethod("some arg")).thenReturn("one", "two", "three");參考:Mockito 中文文檔
三、PowerMockito框架使用
Mockito框架基本滿足需求但是有一些局限性,如對static、final、private等方法不能mock,PowerMockito就可以解決這些問題,PowerMockito是一個擴展了其它如EasyMock等mock框架的、功能更加強大的框架。PowerMock使用一個自定義類加載器和字節碼操作來模擬靜態方法,構造函數,final類和方法,私有方法,去除靜態初始化器等等。
添加依賴:
testImplementation 'org.powermock:powermock-module-junit4:2.0.2'testImplementation 'org.powermock:powermock-module-junit4-rule:2.0.2'testImplementation 'org.powermock:powermock-api-mockito2:2.0.2'testImplementation 'org.powermock:powermock-classloading-xstream:2.0.2'1. 普通Mock的方式
目標類:
public class CommonExample {public boolean callArgumentInstance(File file) {return file.exists();} }測試類:
public class CommonExamplePowerMockTest {@Testpublic void testCallArgumentInstance() {File file = PowerMockito.mock(File.class);CommonExample commonExample = new CommonExample();PowerMockito.when(file.exists()).thenReturn(true);Assert.assertTrue(commonExample.callArgumentInstance(file));}}普通Mock方式是外部傳遞Mock參數,基本上和單獨使用Mockito是一樣的,使用純Mockito的api也可以完成這個測試。
2. Mock方法內部new出來的對象
public class CommonExample {public boolean callArgumentInstance(String path) {File file = new File(path);return file.exists();} } @RunWith(PowerMockRunner.class) @PrepareForTest(CommonExample.class) public class CommonExamplePowerMockTest {@Testpublic void callCallArgumentInstance2() throws Exception {File file = PowerMockito.mock(File.class);CommonExample commonExample = new CommonExample();PowerMockito.whenNew(File.class).withArguments("aaa").thenReturn(file);PowerMockito.when(file.exists()).thenReturn(true);Assert.assertTrue(commonExample.callArgumentInstance("aaa"));} }跟前面有一點區別的就是,這里要測試的方法內部創建了File對象,這時需要通過PowerMockito.whenNew(File.class).withArguments("aaa").thenReturn(file)方法模擬創建File的操作,當File類以aaa的參數創建的時候返回已經mock出來的file對象。同時這時需要在測試類上添加注解@RunWith(PowerMockRunner.class)和@PrepareForTest(CommonExample.class),注意是在類上面添加,不是在方法上,一開始在方法上添加時提示找不到測試方法,@PrepareForTest()括號里面指定的是要測試的目標類。
3. Mock普通對象的final方法
public class CommonExample {public boolean callFinalMethod(DependencyClass dependency) {return dependency.isValidate();} }public class DependencyClass {public final boolean isValidate() {// do somethingreturn false;} } @RunWith(PowerMockRunner.class) @PrepareForTest({CommonExample.class, DependencyClass.class}) public class CommonExamplePowerMockTest {@Testpublic void callFinalMethod() {DependencyClass dependency = PowerMockito.mock(DependencyClass.class);CommonExample commonExample = new CommonExample();PowerMockito.when(dependency.isValidate()).thenReturn(true);Assert.assertTrue(commonExample.callFinalMethod(dependency));} }同樣這里mock出來需要依賴的類的對象,然后傳遞給調用方法,這里同樣需要添加@RunWith和@PrepareForTest,@PrepareForTest可以指定多個目標類,但是這里如果你只需要測試final的話,只添加DependencyClass.class一個就可以了。
4. Mock普通類的靜態方法
public final class Utils {public static String getUUId() {return UUID.randomUUID().toString();} }public class CommonExample {public String printUUID() {return Utils.getUUId();} } @RunWith(PowerMockRunner.class) @PrepareForTest(Utils.class) public class StaticUnitTest {@Beforepublic void setUp() throws Exception {PowerMockito.mockStatic(Utils.class);}@Testpublic void getUUId() {PowerMockito.when(Utils.getUUId()).thenReturn("FAKE UUID");CommonExample commonExample = new CommonExample();assertThat(commonExample.printUUID(), is("FAKE UUID"));} }同樣需要指定@RunWith和@PrepareForTest,@PrepareForTest中指定靜態方法所在的類,測試靜態方法之前需要調用PowerMockito.mockStatic()方法來mock靜態類,然后就通過when().thenReturn()方法指定靜態方法的模擬返回值即可。
5. verify靜態方法的調用次數
@Testpublic void testVerify() {PowerMockito.when(Utils.getUUId()).thenReturn("FAKE UUID");CommonExample commonExample = new CommonExample();System.out.println(commonExample.printUUID());PowerMockito.verifyStatic(Utils.class);Utils.getUUId();}靜態方法通過PowerMockito.verifyStatic(Class c)進行驗證,不過這里跟Mocktio有一點區別的是需要在這個方法的后面再調用一次靜態方法,否則不行。這里PowerMockito.verifyStatic(Utils.class)其實等同于PowerMockito.verifyStatic(Utils.class, times(1)),如果想驗證超過一次的,那么:
@Testpublic void testVerify() {PowerMockito.when(Utils.getUUId()).thenReturn("FAKE UUID");CommonExample commonExample = new CommonExample();System.out.println(commonExample.printUUID());System.out.println(commonExample.printUUID());PowerMockito.verifyStatic(Utils.class, Mockito.times(2));Utils.getUUId();}這時PowerMockito.verifyStatic()第一個參數指定靜態方法類的Class,第二個參數接收一個VerificationMode類型的參數,因此傳遞Mockito中的任何驗證方法次數的函數都可以,Mockito中的驗證函數會返回的是一個VerificationMode類型。同樣在PowerMockito.verifyStatic方法后面要調用一次要驗證的靜態方法,總感覺這里很奇怪。。。
6. 使用真實返回值
如果在測試的過程中又遇到不需要mock出來的靜態方法的模擬返回值,而是需要真實的返回值,怎么辦呢,其實跟Mockito一樣,PowerMockito同樣提供thenCallRealMethod或者doCallRealMethod方法:
@Testpublic void testRealCall() throws Exception {PowerMockito.when(Utils.getUUId()).thenReturn("FAKE UUID");//...PowerMockito.when(Utils.getUUId()).thenCallRealMethod();//與下面等價//PowerMockito.doCallRealMethod().when(Utils.class, "getUUId");System.out.println(Utils.getUUId());}或者直接使用spy監控真實對象也可以:
@Testpublic void testRealCall() {PowerMockito.spy(Utils.class);System.out.println(Utils.getUUId());}7. Mock私有方法
public class CommonExample {public boolean callPrivateMethod() {return isExist();}private boolean isExist() {return false;}} @RunWith(PowerMockRunner.class) @PrepareForTest(CommonExample.class) public class PrivateUnitTest {@Testpublic void testCallPrivateMethod() throws Exception {CommonExample commonExample = PowerMockito.mock(CommonExample.class);PowerMockito.when(commonExample.callPrivateMethod()).thenCallRealMethod();PowerMockito.when(commonExample, "isExist").thenReturn(true);Assert.assertTrue(commonExample.callPrivateMethod());} }在使用上跟純Mockito的沒有太大區別,只不過Mock私有方法是通過下面的api實現的:
PowerMockito.when(Object instance, String methodName, Object... arguments)在PowerMockito中when函數與Mockito相比,最大的變化就是多了一些傳遞String類型的methodName的重載方法,這樣在使用上幾乎無所不能了。
8. Mock普通類的私有變量
public class CommonExample {private static final int STATE_NOT_READY = 0;private static final int STATE_READY = 1;private int mState = STATE_NOT_READY;public boolean doSomethingIfStateReady() {if (mState == STATE_READY) {// DO some thingreturn true;} else {return false;}}} @Testpublic void testDoSomethingIfStateReady() throws Exception {CommonExample sample = new CommonExample();Whitebox.setInternalState(sample, "mState", 1);assertThat(sample.doSomethingIfStateReady(), is(true));}通過Whitebox.setInternalState來改變私有成員變量,這種情況下不需要指定@RunWith和@PrepareForTest。
9. 對靜態void方法進行Mock
public class CommonExample {public static void doSomething(String a) {System.out.println("doSomething"+a);} } @RunWith(PowerMockRunner.class) @PrepareForTest({CommonExample.class}) public class StaticUnitTest {@Testpublic void testStaticVoid() throws Exception {PowerMockito.mockStatic(CommonExample.class);PowerMockito.doNothing().when(CommonExample.class, "doSomething", Mockito.any());CommonExample.doSomething("aaa");} }默認情況下通過PowerMockito.mockStatic的靜態類的void的方法是什么也不做的,但是可以顯示的執行doNothing, 上面的代碼將doNothing那行注釋掉也是什么也不做的。那如果想做一些事而不是doNothing呢,跟Mockito一樣,采用doAnswer:
@Testpublic void testStaticVoid() throws Exception {PowerMockito.mockStatic(CommonExample.class);PowerMockito.doAnswer(new Answer<Object>() {@Overridepublic Object answer(InvocationOnMock invocation) throws Throwable {System.out.println(invocation.getArguments()[0]);return null;}}).when(CommonExample.class, "doSomething", Mockito.any());CommonExample.doSomething("aaa");}10. Mock系統的final靜態類
public class CommonExample {public int callSystemStaticMethod(int a, int b) {return Math.max(a, a+b);} } @RunWith(PowerMockRunner.class) @PrepareForTest(CommonExample.class) public class StaticUnitTest {@Testpublic void callSystemStaticMethod() {CommonExample commonExample = new CommonExample();PowerMockito.mockStatic(Math.class);PowerMockito.when(Math.max(anyInt(), anyInt())).thenReturn(100);Assert.assertEquals(100, commonExample.callSystemStaticMethod(10, -5));} }@PrepareForTest中添加調用系統類所在的類,這里需要注意的是如果你使用PowerMockito來mock系統靜態final類,則gradle依賴中不能再添加單純Mockito的依賴庫,否則這里將不能mock成功,會提示Mockito can not mock/spy final class, 因為PowerMockito本身已經有對Mockito的依賴庫支持了,所以只依賴PowerMockito就可以了。除了系統靜態final類的情況,其他的情況下PowerMockito和Mockito可以同時依賴(我測試是沒有問題的)。另外單純的Mockito新版本中也支持對 final 類 final 方法的 Mock,但是需要添加配置文件并不友好。
四、Robolectric測試框架的使用
由于Robolectric部分的內容比較長,所以單獨放了一篇文章中:Android單元測試框架Robolectric的學習使用
五、Espresso測試框架的使用
Espresso是用于Android儀器化測試的測試框架,是谷歌官方主推的一個測試庫。由于Espresso部分的內容也比較長,所以單獨放了一篇文章中:Espresso測試框架的使用
總結
以上是生活随笔為你收集整理的Android单元测试学习总结的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 9号天气
- 下一篇: android sina oauth2.