Android單元測試主要分為以下兩種
- 本地單元測試(Junit Test), 本地單元測試是純java代碼的測試,只運行在本地電腦的JVM環(huán)境上,不依賴于Android框架的任何api, 因此執(zhí)行速度快,效率較高,但是無法測試Android相關(guān)的代碼。
- 儀器化測試(Android Test),是針對Android相關(guān)代碼的測試,需要運行在真機設備或模擬器上,運行速度較慢,但是可以測試UI的交互以及對設備信息的訪問,得到接近真實的測試結(jié)果。
在Android Studio中新建一個項目的時候,app的gradle中會默認添加單元測試的相關(guān)依賴庫:
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環(huán)境下的測試庫,同時,在項目的工程目錄下也會默認創(chuàng)建好測試的目錄:
其中app/src/test/下面存放的是Junit本地測試代碼,app/src/androidTest/下面存放的是Android測試代碼。
一、本地單元測試
進行本地單元測試需要先了解一些基本的Junit注解:
| 注解名稱 | 含義 |
|---|---|
| @Test | 定義所在方法為單元測試方法,方法必須是public void |
| @Before | 定義所在方法在每個測試用例執(zhí)行之前執(zhí)行一次, 用于準備測試環(huán)境(如: 初始化類,讀輸入流等),在一個測試類中,每個@Test方法的執(zhí)行都會觸發(fā)一次調(diào)用 |
| @After | 定義所在方法在每個測試用例執(zhí)行之后執(zhí)行一次,用于清理測試環(huán)境數(shù)據(jù),在一個測試類中,每個@Test方法的執(zhí)行都會觸發(fā)一次調(diào)用。 |
| @BeforeClass | 定義所在方法在測試類里的所有用例運行之前運行一次,方法必須是public static void,用于做一些耗時的初始化工作(如: 連接數(shù)據(jù)庫) |
| @AfterClass | 定義所在方法在測試類里的所有用例運行之后運行一次,方法必須是public static void,用于清理數(shù)據(jù)(如: 斷開數(shù)據(jù)連接) |
| @Test (expected = Exception.class) | 如果該測試方法沒有拋出Annotation中的Exception類型(子類也可以),則測試失敗 |
| @Test(timeout=100) | 如果該測試方法耗時超過100毫秒,則測試失敗,用于性能測試 |
| @Ignore 或者 @Ignore(“太耗時”) | 忽略當前測試方法,一般用于測試方法還沒有準備好,或者太耗時之類的 |
| @FixMethodOrder | 定義所在的測試類中的所有測試方法都按照固定的順序執(zhí)行,可以指定3個值,分別是DEFAULT、JVM、NAME_ASCENDING(字母順序) |
| @RunWith | 指定測試類的測試運行器 |
更多可以參考Junit官網(wǎng):https://junit.org/junit4/
1. 創(chuàng)建測試類
接下來就可以創(chuàng)建測試類,除了可以手動創(chuàng)建測試類外,可以利用AS快捷鍵:將光標選中要創(chuàng)建測試類的類名上->按下ALT + ENTER->在彈出的彈窗中選擇Create Test
這會彈出下面的彈窗,或者鼠標在類名上右鍵選擇菜單Go to–>Test,也會彈出下面的彈窗
勾選需要進行測試的方法,會自動生成一個測試類:
如果勾選了@Before或@After的話也會自動給你生成對應的測試方法
接下來編寫測試方法,首先在要測試的目標類中寫幾個業(yè)務方法:
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;
@Before
public void setUp() throws Exception {
simpleClass = new SimpleClass();
}
@After
public void tearDown() throws Exception {
}
@Test
public void isTeenager() {
Assert.assertFalse(simpleClass.isTeenager(20));
Assert.assertTrue(simpleClass.isTeenager(14));
}
@Test
public void add() {
Assert.assertEquals(simpleClass.add(3, 2), 5);
Assert.assertNotEquals(simpleClass.add(3, 2), 4);
}
@Test
public void getNameById() {
Assert.assertEquals(simpleClass.getNameById(1), "小明");
Assert.assertEquals(simpleClass.getNameById(2), "小紅");
Assert.assertEquals(simpleClass.getNameById(10), "");
}
}
其中setUp()是自動生成的添加了@Before注解,這會在每個測試方法執(zhí)行前執(zhí)行,因此在這里創(chuàng)建一個目標對象,也可以選擇添加@BeforeClass注解但這時setUp()應該改為靜態(tài)的方法。然后在每個測試方法中編寫測試用例,這里使用org.junit.Assert包中的斷言方法,有很多assertXXX方法,可以自己選擇用來判斷目標方法的結(jié)果是否滿足預期。
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) | 斷言結(jié)果為true |
| assertTrue(String message, boolean condition) | 斷言結(jié)果為true, 為false時拋出異常攜帶指定的message信息 |
| assertFalse(boolean condition) | 斷言結(jié)果為false |
| assertFalse(String message, boolean condition) | 斷言結(jié)果為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) | 斷言兩個相同類型的數(shù)組的元素一一對應相等 |
| assertArrayEquals(String message, T[] expected, T[] actual) | 斷言兩個相同類型的數(shù)組的元素一一對應相等,如果不相等則拋出異常攜帶指定的message信息 |
| fail() | 直接讓測試失敗 |
| fail(String message) | 直接讓測試失敗并給出message錯誤信息 |
| assertThat(T actual, Matcher<? super T> matcher) | 斷言actual和matcher規(guī)則匹配 |
| assertThat(String reason, T actual, Matcher<? super T> matcher) | 斷言actual和matcher規(guī)則匹配,否則拋出異常攜帶指定的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"));
需要靜態(tài)導入org.hamcrest.Matchers類里面的方法,更多匹配方法請參考這個類。
3. 運行測試類
選中測試類右鍵Run運行,控制面板中就會顯示測試結(jié)果:
如果所有的測試用例都正常返回了預期的結(jié)果,則面板中左側(cè)每個測試方法前面會帶一個綠色的對勾,否則方法前面會變成紅色感嘆號并且控制面板會輸出異常,現(xiàn)在來改一個業(yè)務方法試一下:
public boolean isTeenager(int age) {
if (age < 15) {
return false;
}
return false;
}
這里將age < 15改為輸出false,假設這是我們在編碼的時候由于疏忽粗心造成的,然后運行測試類:
控制面板會告訴那一行出錯了:

也就是說這里沒有返回預期的結(jié)果,說明我們編寫的業(yè)務邏輯是有錯誤的,這時就需要改bug了。
4. 運行單個測試方法或多個測試類
上面是運行的整個測試類,如果要運行測試類的單個方法,則鼠標只選中某個要運行的測試方法,然后右鍵選擇Run即可。如果要同時運行多個測試類,而如果多個測試類在同一個包下面,則選中多個測試類所在的包目錄,然后右鍵選擇Run運行。否則可以通過下面的方式指定,創(chuàng)建一個空的測試類,然后添加注解:
@RunWith(Suite.class)
@Suite.SuiteClasses({SimpleClassTest.class, SimpleClass2Test.class})
public class RunMultiTest {
}
運行這個測試類就可以將指定的測試類的方法一起運行。
二、Mockito測試框架的使用
前面介紹的只能測試不涉及Android相關(guān)Api的java代碼用例,如果涉及到Android相關(guān)Api的時候,就不方便了,這時如果不依賴第三方庫的話可能需要使用儀器化測試跑到Android設備上去運行,于是有一些比較好的第三方的替代框架可以來模擬使用Android的代碼測試,Mockito就是基于依賴注入實現(xiàn)的一個測試框架。
1. Mock概念的理解
什么是Mock, 這個單詞的中文意思就是“模仿”或者“虛假”的意思,也就是要模仿一個對象,為啥要模仿?
在傳統(tǒng)的JUnit單元測試中,沒有消除在測試中對對象的依賴,如A對象依賴B對象方法,在測試A對象的時候,我們需要構(gòu)造出B對象,這樣子增加了測試的難度,或者使得我們對某些類的測試無法實現(xiàn)。這與單元測試的思路相違背。
還有一個主要的問題就是本地單元測試由于是運行本地JVM環(huán)境,無法依賴Android的api,只靠純Junit的測試環(huán)境很難模擬出完整的Android環(huán)境,導致無法測試Android相關(guān)的代碼,而Mock就能解決這個問題,通過Mock能夠很輕易的實現(xiàn)對象的模擬。
添加依賴:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
testImplementation 'org.mockito:mockito-core:2.19.0'
....
}
- Mockito中幾種Mock對象的方式
使用之前通過靜態(tài)方式導入會使用更方便:
// 靜態(tài)導入會使代碼更簡潔
import static org.mockito.Mockito.*;
直接mock一個對象:
@Test
public void testMock() {
SimpleClass mockSimple = Mockito.mock(SimpleClass.class);
assertNotNull(mockSimple);
}
注解方式mock一個對象:
@Mock
SimpleClass simple;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
@Test
public void testMock() {
assertNotNull(simple);
}
運行器方式mock一個對象:
@RunWith(MockitoJUnitRunner.class)
public class ExampleUnitTest {
@Mock
SimpleClass simple;
@Test
public void testMock() {
assertNotNull(simple);
}
}
MockitoRule方式mock一個對象:
public class ExampleUnitTest {
@Mock
SimpleClass simple;
@Rule //<--使用@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
@Test
public void testMock() {
assertNotNull(simple);
}
}
3. 驗證行為
verify(T mock)函數(shù)的使用
verify(T mock)的作用是驗證發(fā)生的某些行為等同于verify(mock, times(1)) 例如:
@Test
public void testMock() {
//創(chuàng)建mock對象
List mockedList = mock(List.class);
//使用mock對象
mockedList.add("one");
mockedList.clear();
//驗證mockedList.add("one")是否被調(diào)用,如果被調(diào)用則當前測試方法通過,否則失敗
verify(mockedList).add("one");
//驗證 mockedList.clear()是否被調(diào)用,如果被調(diào)用則當前測試方法通過,否則失敗
verify(mockedList).clear();
}
@Test
public void testMock() {
mock.someMethod("some arg");
//驗證mock.someMethod("some arg")是否被調(diào)用,如果被調(diào)用則測試方法通過,否則失敗
verify(mock).someMethod("some arg");
}
也就是說如果把調(diào)用的方法注釋掉,則運行testMock()方法就會失敗。
通過verify關(guān)鍵字,一旦mock對象被創(chuàng)建了,mock對象會記住所有的交互。然后你就可能選擇性的驗證你感興趣的交互。
通常需要配合一些測試方法來驗證某些行為,這些方法稱為"打樁方法"(Stub),打樁的意思是針對mock出來的對象進行一些模擬操作,如設置模擬的返回值或拋出異常等。
常見的打樁方法:
| 方法名 | 方法含義 |
|---|---|
| doReturn(Object toBeReturned) | 提前設置要返回的值 |
| doThrow(Throwable… toBeThrown) | 提前設置要拋出的異常 |
| doAnswer(Answer answer) | 提前對結(jié)果進行攔截 |
| doCallRealMethod() | 調(diào)用某一個方法的真實實現(xiàn) |
| doNothing() | 設置void函數(shù)什么也不做 |
| thenReturn(T value) | 設置要返回的值 |
| thenThrow(Throwable… throwables) | 設置要拋出的異常 |
| thenAnswer(Answer<?> answer) | 對結(jié)果進行攔截 |
例如:
@Test
public 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, 注意模擬環(huán)境下這個地方是不會報IndexOutOfBoundsException異常的
System.out.println(mockedList.get(999));
// get(1)時會拋出異常
System.out.println(mockedList.get(1));
// clear會拋出異常
mockedList.clear();
}
doXXX和thenXXX使用上差不多,一個是調(diào)用方法之前設置好返回值,一個是在調(diào)用方法之后設置返回值。默認情況下,Mock出的對象的所有非void函數(shù)都有返回值,對象類型的默認返回的是null,例如返回int、boolean、String的函數(shù),默認返回值分別是0、false和null。
使用when(T methodCall)函數(shù)
打樁方法需要配合when(T methodCall)函數(shù),意思是使測試樁方法生效。當你想讓這個mock能調(diào)用特定的方法返回特定的值,那么你就可以使用它。
例如:
when(mock.someMethod()).thenReturn(10);
//你可以使用靈活的參數(shù)匹配,例如
when(mock.someMethod(anyString())).thenReturn(10);
//設置拋出的異常
when(mock.someMethod("some arg")).thenThrow(new RuntimeException());
//你可以對不同作用的連續(xù)回調(diào)的方法打測試樁:
//最后面的測試樁(例如:返回一個對象:"foo")決定了接下來的回調(diào)方法以及它的行為。
when(mock.someMethod("some arg"))
.thenReturn("foo")//第一次調(diào)用someMethod("some arg")會返回"foo"
.thenThrow(new RuntimeException());//第二次調(diào)用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為回調(diào)做測試樁
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()函數(shù)來調(diào)用某個方法的真實實現(xiàn)方法
注意,在Mock環(huán)境下,所有的對象都是模擬出來的,而方法的結(jié)果也是需要模擬出來的,如果你沒有為mock出的對象設置模擬結(jié)果,則會返回默認值,例如:
public class Person {
public String getName() {
return "小明";
}
}
@Test
public void testPerson() {
Person mock = mock(Person.class);
//輸出null,除非設置發(fā)回模擬值when(mock.getName()).thenReturn("xxx");
System.out.println(mock.getName());
}
因為getName()方法沒有設置模擬返回值,而getName()返回值是String類型的,因此直接調(diào)用的話會返回String的默認值null,所以上面代碼如果要想輸出getName()方法的真實返回值的話,需要設置doCallRealMethod():
@Test
public void testPerson() {
Person mock = mock(Person.class);
doCallRealMethod().when(mock).getName();
//輸出“小明”
System.out.println(mock.getName());
}
使用doNothing()函數(shù)是為了設置void函數(shù)什么也不做
需要注意的是默認情況下返回值為void的函數(shù)在mocks中是什么也不做的但是,也會有一些特殊情況。如:
測試樁連續(xù)調(diào)用一個void函數(shù)時:
doNothing().doThrow(new RuntimeException()).when(mock).someVoidMethod();
//does nothing the first time:
mock.someVoidMethod();
//throws RuntimeException the next time:
mock.someVoidMethod();
監(jiān)控真實的對象并且你想讓void函數(shù)什么也不做:
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()函數(shù)測試void函數(shù)的回調(diào)
當你想要測試一個無返回值的函數(shù)時,可以使用一個含有泛型類Answer參數(shù)的doAnswer()函數(shù)做回調(diào)測試。假設你有一個void方法有多個回調(diào)參數(shù),當你想指定執(zhí)行某個回調(diào)時,使用thenAnswer很難實現(xiàn)了,如果使用doAnswer()將非常簡單,示例代碼如下:
MyCallback callback = mock(MyCallback.class);
Mockito.doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
//獲取第一個參數(shù)
MyCallback call = invocation.getArgument(0);
//指定回調(diào)執(zhí)行操作
call.onSuccess();
return null;
}
}).when(mockedObject.requset(callback));
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
System.out.println("onSuccess answer");
return null;
}
}).when(callback).onSuccess();
mockedObject.requset(callback)
需要使用doReturn函數(shù)代替thenReturn的情況
如當監(jiān)控真實的對象并且調(diào)用真實的函數(shù)帶來的影響時
List list = new LinkedList();
List spy = spy(list);
//不可能完成的:真實方法被調(diào)用的時候list仍是空的,所以spy.get(0)會拋出IndexOutOfBoundsException()異常
when(spy.get(0)).thenReturn("foo");
//這時你應該使用doReturn()函數(shù)
doReturn("foo").when(spy).get(0);
使用doThrow()函數(shù)來測試void函數(shù)拋出異常
SimpleClass mock = mock(SimpleClass.class);
doThrow(new RuntimeException()).when(mock).someVoidMethod();
mock.someVoidMethod();
總之使用doThrow(), doAnswer(), doNothing(), doReturn() and doCallRealMethod() 這些函數(shù)時可以在適當?shù)那闆r下調(diào)用when()來解決一些問題., 如當你需要下面這些功能時這是必須的:
- 測試void函數(shù)
- 在受監(jiān)控的對象上測試函數(shù)
- 不只一次的測試同一個函數(shù),在測試過程中改變mock對象的行為
4. 驗證方法的調(diào)用次數(shù)
需要配合使用一些方法
| 方法 | 含義 |
|---|---|
| times(int wantedNumberOfInvocations) | 驗證調(diào)用方法的次數(shù) |
| never() | 驗證交互沒有發(fā)生,相當于times(0) |
| only() | 驗證方法只被調(diào)用一次,相當于times(1) |
| atLeast(int minNumberOfInvocations) | 至少進行n次驗證 |
| atMost(int maxNumberOfInvocations) | 至多進行n次驗證 |
| after(long millis) | 在給定的時間后進行驗證 |
| timeout(long millis) | 驗證方法執(zhí)行是否超時 |
| description(String description) | 驗證失敗時輸出的內(nèi)容 |
| verifyZeroInteractions | 驗證mock對象沒有交互 |
例如:
mock.someMethod("some arg");
mock.someMethod("some arg");
//驗證mock.someMethod("some arg")被連續(xù)調(diào)用兩次,即如果沒有調(diào)用兩次則驗證失敗
verify(mock, times(2)).someMethod("some arg");
//注意,下面三種是等價的,都是驗證someMethod()被只調(diào)用一次
verify(mock).someMethod("some arg");
verify(mock, times(1)).someMethod("some arg");
verify(mock, only()).someMethod("some arg");
mPerson.getAge();
mPerson.getAge();
//驗證至少調(diào)用2次
verify(mPerson, atLeast(2)).getAge();
//驗證至多調(diào)用2次
verify(mPerson, atMost(2)).getAge();
//下面兩種等價,驗證調(diào)用次數(shù)為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()是否被執(zhí)行了2次
verify(mPerson, after(1000).times(2)).getAge();
System.out.println(System.currentTimeMillis() - current);
mPerson.getAge();
mPerson.getAge();
//驗證方法在100ms超時前被調(diào)用2次
verify(mPerson, timeout(100).times(2)).getAge();
@Test
public void testVerifyZeroInteractions() {
Person person = mock(Person.class);
person.eat("a");
//由于person對象發(fā)生了交互,所以這里驗證失敗,把上面的調(diào)用注釋掉這里就會驗證成功
verifyZeroInteractions(person);
//可以驗證多個對象沒有交互
//verifyZeroInteractions(person,person2 );
}
@Test
public void testVerifyZeroInteractions() {
Person person = mock(Person.class);
person.eat("a");
verify(person).eat("a");
//注意,這將會無法到達驗證目的,不能跟verify()混用
verifyZeroInteractions(person,person2 );
}
5. 參數(shù)匹配器 (matchers)
Mockito以自然的java風格來驗證參數(shù)值: 使用equals()函數(shù)。有時,當需要額外的靈活性時你可能需要使用參數(shù)匹配器,也就是argument matchers :
// 使用內(nèi)置的anyInt()參數(shù)匹配器
when(mockedList.get(anyInt())).thenReturn("element");
// 使用自定義的參數(shù)匹配器( 在isValid()函數(shù)中返回你自己的匹配器實現(xiàn) )
when(mockedList.contains(argThat(isValid()))).thenReturn("element");
// 輸出element
System.out.println(mockedList.get(999));
// 你也可以驗證參數(shù)匹配器
verify(mockedList).get(anyInt());
常用的參數(shù)匹配器:
| 方法名 | 含義 |
|---|---|
| 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) | 參數(shù)包含給定的substring字符串 |
| argThat(ArgumentMatcher matcher) | 創(chuàng)建自定義的參數(shù)匹配模式 |
| eq(T value) | 匹配參數(shù)等于某個值 |
一些示例代碼:
@Test
public void testPersonAny(){
when(mPerson.eat(any(String.class))).thenReturn("米飯");
//或:
when(mPerson.eat(anyString())).thenReturn("米飯");
//輸出米飯
System.out.println(mPerson.eat("面條"));
}
@Test
public void testPersonContains(){
when(mPerson.eat(contains("面"))).thenReturn("面條");
//輸出面條
System.out.println(mPerson.eat("面"));
}
@Test
public void testPersonArgThat(){
//自定義輸入字符長度為偶數(shù)時,輸出面條。
when(mPerson.eat(argThat(new ArgumentMatcher<String>() {
@Override
public boolean matches(String argument) {
return argument.length() % 2 == 0;
}
}))).thenReturn("面條");
//輸出面條
System.out.println(mPerson.eat("1234"));
}
需要注意的是,如果你打算使用參數(shù)匹配器,那么所有參數(shù)都必須由匹配器提供。例如:
verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));
// 上述代碼是正確的,因為eq()也是一個參數(shù)匹配器
verify(mock).someMethod(anyInt(), anyString(), "third argument");
// 上述代碼是錯誤的, 因為所有參數(shù)必須由匹配器提供,而參數(shù)"third argument"并非由參數(shù)匹配器提供,因此會拋出異常
像anyObject(), eq()這樣的匹配器函數(shù)不會返回匹配器。它們會在內(nèi)部將匹配器記錄到一個棧當中,并且返回一個假的值,通常為null。
6. 使用InOrder驗證執(zhí)行執(zhí)行順序
驗證執(zhí)行執(zhí)行順序主要使用InOrder函數(shù)
如,驗證mock一個對象的函數(shù)執(zhí)行順序:
@Test
public void testInorder() {
List<String> singleMock = mock(List.class);
singleMock.add("小明");
singleMock.add("小紅");
// 為該mock對象創(chuàng)建一個inOrder對象
InOrder inOrder = inOrder(singleMock);
// 驗證add函數(shù)首先執(zhí)行的是add("小明"),然后才是add("小紅"),否則測試失敗
inOrder.verify(singleMock).add("小明");
inOrder.verify(singleMock).add("小紅");
}
驗證多個mock對象的函數(shù)執(zhí)行順序:
@Test
public void testInorderMulti() {
List<String> firstMock = mock(List.class);
List<String> secondMock = mock(List.class);
firstMock.add("小明");
secondMock.add("小紅");
// 為這兩個Mock對象創(chuàng)建inOrder對象
InOrder inOrder = inOrder(firstMock, secondMock);
// 驗證它們的執(zhí)行順序
inOrder.verify(firstMock).add("小明");
inOrder.verify(secondMock).add("小紅");
}
驗證執(zhí)行順序是非常靈活的,你不需要一個一個的驗證所有交互,只需要驗證你感興趣的對象即可。 你可以選擇單個mock對象和多個mock對象混合著來,也可以僅通過那些需要驗證順序的mock對象來創(chuàng)建InOrder對象。
7. 使用Spy監(jiān)控真實對象
監(jiān)控真實對象使用spy()函數(shù)生成,或者也可以像@Mock那樣使用@Spy注解來生成一個監(jiān)控對象, 當你你為真實對象創(chuàng)建一個監(jiān)控(spy)對象后,在你使用這個spy對象時真實的對象也會也調(diào)用,除非它的函數(shù)被stub了。盡量少使用spy對象,使用時也需要小心形式。
@Test
public void testSpy() {
List<String> list = new ArrayList<>();
List<String> spy = spy(list);
// 你可以選擇為某些函數(shù)打樁
when(spy.size()).thenReturn(100);
// 調(diào)用真實對象的函數(shù)
spy.add("one");
spy.add("two");
// 輸出第一個元素"one"
System.out.println(spy.get(0));
// 因為size()函數(shù)被打樁了,因此這里返回的是100
System.out.println(spy.size());
// 驗證交互
verify(spy).add("one");
verify(spy).add("two");
}
使用@Spy生成監(jiān)控對象:
@Spy
Person mSpyPerson;
@Test
public void testSpyPerson() {
//將會輸出Person 類中g(shù)etName()的真實實現(xiàn),而不是null
System.out.println(mSpyPerson.getName());
}
理解監(jiān)控真實對象非常重要!有時,在監(jiān)控對象上使用when(Object)來進行打樁是不可能或者不切實際的。因此,當使用監(jiān)控對象時請考慮doReturn|Answer|Throw()函數(shù)族來進行打樁。例如:
List list = new LinkedList();
List spy = spy(list);
// 不可能實現(xiàn) : 因為當調(diào)用spy.get(0)時會調(diào)用真實對象的get(0)函數(shù),
// 此時會發(fā)生IndexOutOfBoundsException異常,因為真實List對象是空的
when(spy.get(0)).thenReturn("foo");
// 你需要使用doReturn()來打樁
doReturn("foo").when(spy).get(0);
8. 使用ArgumentCaptor進行參數(shù)捕獲
參數(shù)捕獲主要為了下一步的斷言做準備,示例代碼:
@Test
public void argumentCaptorTest() {
List<Object> mock = mock(List.class);
mock.add("John");
//構(gòu)建要捕獲的參數(shù)類型,這里是String
ArgumentCaptor argument = ArgumentCaptor.forClass(String.class);
//在verify方法的參數(shù)中調(diào)用argument.capture()方法來捕獲輸入的參數(shù)
verify(mock).add(argument.capture());
//驗證“John”參數(shù)捕獲
assertEquals("John", argument.getValue());
}
@Test
public 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());
//如果又多次參數(shù)調(diào)用,argument.getValue()捕獲到的是最后一次調(diào)用的參數(shù)
assertEquals("Jim", argument.getValue());
//如果要獲取所有的參數(shù)值可以調(diào)用argument.getAllValues()
assertArrayEquals(new Object[]{"Brian","Jim"}, argument.getAllValues().toArray());
}
9. 使用@InjectMocks自動注入依賴對象
有時我們要測試的對象內(nèi)部需要依賴另一個對象,例如:
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類內(nèi)部需要依賴Address類,當我們測試的時候需要mock出這兩個對象,然后將Address對象傳入到User當中,這樣如果依賴的對象多了的話就相當麻煩,Mockito 提供了可以不用去手動注入對象的方法,首先使用@InjectMocks注解需要被注入的對象,如User,然后需要被依賴注入的對象使用@Mock或@Spy注解,之后Mockito 會自動完成注入過程,例如:
@InjectMocks
User mTestUser;
@Mock
Address mAddress;
@Test
public void argumentInjectMock() {
when(mAddress.getDetail()).thenReturn("浙江杭州");
System.out.println(mTestUser.getAddress());
}
這樣就不用關(guān)心為User 設置Address ,只要為User需要依賴的類添加注解就可以了,然后直接將重點放到測試方法的編寫上。
或者使用@Spy監(jiān)控真實對象注入也可以:
@InjectMocks
User mTestUser;
@Spy
Address mAddress;
@Test
public void argumentInjectMock() {
// when(mAddress.getDetail()).thenReturn("浙江杭州");
System.out.println(mTestUser.getAddress());
}
其他:
連續(xù)調(diào)用的另一種更簡短的版本:
// 第一次調(diào)用時返回"one",第二次返回"two",第三次返回"three"
when(mock.someMethod("some arg")).thenReturn("one", "two", "three");
參考:Mockito 中文文檔
三、PowerMockito框架使用
Mockito框架基本滿足需求但是有一些局限性,如對static、final、private等方法不能mock,PowerMockito就可以解決這些問題,PowerMockito是一個擴展了其它如EasyMock等mock框架的、功能更加強大的框架。PowerMock使用一個自定義類加載器和字節(jié)碼操作來模擬靜態(tài)方法,構(gòu)造函數(shù),final類和方法,私有方法,去除靜態(tài)初始化器等等。
添加依賴:
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 {
@Test
public 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參數(shù),基本上和單獨使用Mockito是一樣的,使用純Mockito的api也可以完成這個測試。
2. Mock方法內(nèi)部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 {
@Test
public 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"));
}
}
跟前面有一點區(qū)別的就是,這里要測試的方法內(nèi)部創(chuàng)建了File對象,這時需要通過PowerMockito.whenNew(File.class).withArguments("aaa").thenReturn(file)方法模擬創(chuàng)建File的操作,當File類以aaa的參數(shù)創(chuàng)建的時候返回已經(jīng)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 something
return false;
}
}
@RunWith(PowerMockRunner.class)
@PrepareForTest({CommonExample.class, DependencyClass.class})
public class CommonExamplePowerMockTest {
@Test
public void callFinalMethod() {
DependencyClass dependency = PowerMockito.mock(DependencyClass.class);
CommonExample commonExample = new CommonExample();
PowerMockito.when(dependency.isValidate()).thenReturn(true);
Assert.assertTrue(commonExample.callFinalMethod(dependency));
}
}
同樣這里mock出來需要依賴的類的對象,然后傳遞給調(diào)用方法,這里同樣需要添加@RunWith和@PrepareForTest,@PrepareForTest可以指定多個目標類,但是這里如果你只需要測試final的話,只添加DependencyClass.class一個就可以了。
4. Mock普通類的靜態(tài)方法
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 {
@Before
public void setUp() throws Exception {
PowerMockito.mockStatic(Utils.class);
}
@Test
public void getUUId() {
PowerMockito.when(Utils.getUUId()).thenReturn("FAKE UUID");
CommonExample commonExample = new CommonExample();
assertThat(commonExample.printUUID(), is("FAKE UUID"));
}
}
同樣需要指定@RunWith和@PrepareForTest,@PrepareForTest中指定靜態(tài)方法所在的類,測試靜態(tài)方法之前需要調(diào)用PowerMockito.mockStatic()方法來mock靜態(tài)類,然后就通過when().thenReturn()方法指定靜態(tài)方法的模擬返回值即可。
5. verify靜態(tài)方法的調(diào)用次數(shù)
@Test
public void testVerify() {
PowerMockito.when(Utils.getUUId()).thenReturn("FAKE UUID");
CommonExample commonExample = new CommonExample();
System.out.println(commonExample.printUUID());
PowerMockito.verifyStatic(Utils.class);
Utils.getUUId();
}
靜態(tài)方法通過PowerMockito.verifyStatic(Class c)進行驗證,不過這里跟Mocktio有一點區(qū)別的是需要在這個方法的后面再調(diào)用一次靜態(tài)方法,否則不行。這里PowerMockito.verifyStatic(Utils.class)其實等同于PowerMockito.verifyStatic(Utils.class, times(1)),如果想驗證超過一次的,那么:
@Test
public 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()第一個參數(shù)指定靜態(tài)方法類的Class,第二個參數(shù)接收一個VerificationMode類型的參數(shù),因此傳遞Mockito中的任何驗證方法次數(shù)的函數(shù)都可以,Mockito中的驗證函數(shù)會返回的是一個VerificationMode類型。同樣在PowerMockito.verifyStatic方法后面要調(diào)用一次要驗證的靜態(tài)方法,總感覺這里很奇怪。。。
6. 使用真實返回值
如果在測試的過程中又遇到不需要mock出來的靜態(tài)方法的模擬返回值,而是需要真實的返回值,怎么辦呢,其實跟Mockito一樣,PowerMockito同樣提供thenCallRealMethod或者doCallRealMethod方法:
@Test
public 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監(jiān)控真實對象也可以:
@Test
public 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 {
@Test
public 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的沒有太大區(qū)別,只不過Mock私有方法是通過下面的api實現(xiàn)的:
PowerMockito.when(Object instance, String methodName, Object... arguments)
在PowerMockito中when函數(shù)與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 thing
return true;
} else {
return false;
}
}
}
@Test
public void testDoSomethingIfStateReady() throws Exception {
CommonExample sample = new CommonExample();
Whitebox.setInternalState(sample, "mState", 1);
assertThat(sample.doSomethingIfStateReady(), is(true));
}
通過Whitebox.setInternalState來改變私有成員變量,這種情況下不需要指定@RunWith和@PrepareForTest。
9. 對靜態(tài)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 {
@Test
public void testStaticVoid() throws Exception {
PowerMockito.mockStatic(CommonExample.class);
PowerMockito.doNothing().when(CommonExample.class, "doSomething", Mockito.any());
CommonExample.doSomething("aaa");
}
}
默認情況下通過PowerMockito.mockStatic的靜態(tài)類的void的方法是什么也不做的,但是可以顯示的執(zhí)行doNothing, 上面的代碼將doNothing那行注釋掉也是什么也不做的。那如果想做一些事而不是doNothing呢,跟Mockito一樣,采用doAnswer:
@Test
public void testStaticVoid() throws Exception {
PowerMockito.mockStatic(CommonExample.class);
PowerMockito.doAnswer(new Answer<Object>() {
@Override
public 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系統(tǒng)的final靜態(tài)類
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 {
@Test
public 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中添加調(diào)用系統(tǒng)類所在的類,這里需要注意的是如果你使用PowerMockito來mock系統(tǒng)靜態(tài)final類,則gradle依賴中不能再添加單純Mockito的依賴庫,否則這里將不能mock成功,會提示Mockito can not mock/spy final class, 因為PowerMockito本身已經(jīng)有對Mockito的依賴庫支持了,所以只依賴PowerMockito就可以了。除了系統(tǒng)靜態(tài)final類的情況,其他的情況下PowerMockito和Mockito可以同時依賴(我測試是沒有問題的)。另外單純的Mockito新版本中也支持對 final 類 final 方法的 Mock,但是需要添加配置文件并不友好。
四、Robolectric測試框架的使用
由于Robolectric部分的內(nèi)容比較長,所以單獨放了一篇文章中:Android單元測試框架Robolectric的學習使用
五、Espresso測試框架的使用
Espresso是用于Android儀器化測試的測試框架,是谷歌官方主推的一個測試庫。由于Espresso部分的內(nèi)容也比較長,所以單獨放了一篇文章中:Espresso測試框架的使用