前面花了很大篇幅來介紹JUnit4,JUnit4是整個單元測試的基礎,其他的測試架構都是跑在JUnit4上的。接下來我們将來學習怎麼樣在Android的單元測試中內建Mockito。
6.1 Mockito介紹
6.1.1 Mockito是什麼?
Mockito是一個用于java單元測試中的mocking架構,mock就是模拟的意思,就是能夠模拟一些類和方法的實作。
其官網位址:
http://site.mockito.org6.1.2 為什麼需要mock?
在寫單元測試的時候,我們會遇到某個測試類有很多依賴,這些依賴類或對象又有别的依賴,這樣會形成一棵巨大的依賴樹,要在單元測試的環境中完整地建構這樣的依賴,是及其困難的,有時候甚至因為運作環境的關系,幾乎不可能完整地建構出這些依賴。如下圖所示:
如果我們要針對ClassA來寫單元測試,發現ClassA依賴ClassB和ClassD,ClassB又依賴ClassC和ClassE,ClassE又依賴ClassF。好吧,寫到這裡我自己都頭疼了,我僅僅是想為ClassA寫一個單元測試而已,卻不得不自己構造這麼多依賴對象,太複雜了。
由上圖可以看到,ClassA隻依賴ClassB和ClassD,我們實際上隻需要構造這2個依賴對象,這2個依賴對象分别實作了ClassB和ClassD的所有功能。ClassA并不關心ClassB的依賴對象ClassC和ClassE是怎麼構造,它隻關心ClassB和ClassD的構造,并且也無需關系他們的實作細節,有沒什麼方法能自動幫我們實作呢,那這樣我們編寫單元測試就容易得多了,Mockito架構就是為了解決這個問題而設計的。
如上圖所示,Mockito架構自動幫我們構造了2個mock對象:MockClassB和MockClassD,這樣ClassA的單元測試就簡單多了。
Mock測試就是在測試過程中,對于一些由于運作環境原因不能構造的對象、或者構造比較複雜的對象、或者我們并不需要關注的對象,用一個虛拟的對象(Mock對象)來替代進而友善測試的測試方法。
6.1.3 在Android中使用Mockito
在build.gradle中加入mockito依賴配置:
testCompile 'org.mockito:mockito-core:2.8.9'
最新版本可以去官網檢視
6.2 使用Mockito
幾乎所有的測試方法都在org.mockito.Mockito類中:
6.2.1 驗證行為
@Test
public void testMock() {
//建立一個mock對象
List list = mock(List.class);
//使用mock對象
list.add("one");
list.clear();
//驗證mock對象的行為
verify(list).add("one"); //驗證有add("one")行為發生
verify(list).clear(); //驗證有clear()行為發生
}
一旦建立一個mock對象,它會記住所有的互動,這樣我們就可以驗證自己感興趣的行為。
6.2.2 Stubbing
Stub對象用來提供測試時所需要的測試資料,對各種互動設定相應的回應。Mockito使用when(...).thenReturn(...)設定方法調用的傳回值,使用when(...).thenThrow(...)設定方法調用時抛出的異常。
@Test
public void testMock2() {
//不僅可以針對接口mock, 還可以針對具體類
LinkedList list = mock(LinkedList.class);
//設定傳回值,當調用list.get(0)時會傳回"first"
when(list.get(0)).thenReturn("first");
//當調用list.get(1)時會抛出異常
when(list.get(1)).thenThrow(new RuntimeException());
//會列印"print"
System.out.println(list.get(0));
//會抛出RuntimeException
System.out.println(list.get(1));
//會列印 null
System.out.println(list.get(99));
verify(list).get(0);
}
對于stubbing,需要注意一下幾點:
- 對于有傳回值的方法,mock會預設傳回null、空集合、預設值。比如為int/Integer傳回0,為boolean/Boolean傳回false、為Object傳回null。
- 一旦stubbing,不管方法被調用多少次,都永遠傳回stubbing的值。
- stubbing可以被覆寫, 如果對同一個方法進行多次stubbing,最後一次的stubbing會生效。
6.2.3 Argument matchers(參數比對器)
@Test
public void testMock3() {
List list = mock(List.class);
//使用anyInt(), anyString(), anyLong()等進行參數比對
when(list.get(anyInt())).thenReturn("item");
//将會列印出"item"
System.out.println(list.get(100));
verify(list).get(anyInt());
}
6.2.4 驗證方法的調用次數
@Test
public void testMock4() {
List list = mock(List.class);
list.add("once");
list.add("twice");
list.add("twice");
list.add("triple");
list.add("triple");
list.add("triple");
//執行1次
verify(list, times(1)).add("once");
//執行2次
verify(list, times(2)).add("twice");
verify(list, times(3)).add("triple");
//從不執行, never()等同于times(0)
verify(list, never()).add("never happened");
//驗證至少執行1次
verify(list, atLeastOnce()).add("twice");
//驗證至少執行2次
verify(list, atLeast(2)).add("twice");
//驗證最多執行4次
verify(list, atMost(4)).add("triple");
}
times(n):方法被調用n次
never():沒有被調用
atLeast(n):至少被調用n次
atLeastOnce():至少被調用1次,相當于atLeast(1)
atMost():最多被調用n次
6.2.5 驗證方法的調用順序
@Test
public void testMock5() {
List list = mock(List.class);
list.add("first");
list.add("second");
InOrder myOrder = inOrder(list);
myOrder.verify(list).add("first");
myOrder.verify(list).add("second");
}
可同時驗證多個mock對象的測試方法的執行順序:
InOrder myOrder = inOrder(firstMock, secondMock, ...)
6.2.6 verifyZeroInteractions && verifyNoMoreInteractions
@Test
public void testMock6() {
List list = mock(List.class);
//驗證mock對象沒有産生任何互動,也即沒有任何方法調用
verifyZeroInteractions(list);
List list2 = mock(List.class);
list2.add("one");
list2.add("two");
verify(list2).add("one");
//驗證mock對象是否有被調用過但沒被驗證的方法。這裡會測試不通過,list2.add("two")方法沒有被驗證過
verifyNoMoreInteractions(list2);
}
6.2.7 使用@Mock建立mock對象
//通過注解會自動建立mock對象
@Mock
private List mockList;
@Mock
private Map mockMap;
要使用@Mock注解有2種配置方式:
- 在base class中或者初始化的地方配置:
MockitoAnnotations.initMocks(this);
- 使用JUnit4的rule來配置:
@Rule
public MockitoRule rule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
以上2種方式可以達到同樣的效果。
6.2.8 do(...).when(...)
- doThrow(Throwable...):進行異常測試
@Test
public void testMock7() {
List list = mock(List.class);
list.add("123");
//當list調用clear()方法時會抛出異常
doThrow(new RuntimeException()).when(list).clear();
list.clear();
}
- doReturn():指定傳回值
@Test
public void testMock8() {
List list = mock(List.class);
doReturn("123").when(list).get(anyInt());
System.out.println(list.get(0));
}
- doNothing() :指定void方法什麼都不做
- doCallRealMethod():指定方法調用内部的真實邏輯
class Foo {
public void doFoo() {
System.out.println("method doFoo called.");
}
public int getCount() {
return 1;
}
}
@Test
public void testMock9() {
Foo foo = mock(Foo.class);
//什麼資訊也不會列印, mock對象并不會調用真實邏輯
foo.doFoo();
//啥也不會列印出來
doNothing().when(foo).doFoo();
foo.doFoo();
doCallRealMethod().when(foo).doFoo();
//這裡會調用真實邏輯, 列印出"method doFoo called."資訊
foo.doFoo();
//這裡會列印出0
System.out.println(foo.getCount());
doCallRealMethod().when(foo).getCount();
//這裡會列印出"1"
System.out.println(foo.getCount());
}
6.2.9 使用spy()監視真正的對象
使用spy可以監視對象方法的真實調用。當我們mock某個類時,如果需要某些方法是真實調用,而某些方法是mock調用時,借助spy可以實作這些功能。
@Test
public void testMock10(){
List list = new ArrayList();
List spy = spy(list);
//subbing方法,size()并不會真實調用,這裡傳回10
when(spy.size()).thenReturn(10);
//使用spy對象會調用真實的方法
spy.add("one");
spy.add("two");
//會列印出"one"
System.out.println(spy.get(0));
//會列印出"10",與前面的stubbing方法對應
System.out.println(spy.size());
//對spy對象依舊可以來驗證其行為
verify(spy).add("one");
verify(spy).add("two");
}
6.2.10 參數捕捉
@Test
public void testMock11() {
List list = mock(List.class);
ArgumentCaptor<String> args = ArgumentCaptor.forClass(String.class);
list.add("one");
//驗證後再捕捉參數
verify(list).add(args.capture());
Assert.assertEquals("one", args.getValue());
}
6.2.11 重置mocks
@Test
public void testMock12() {
List list = mock(List.class);
when(list.size()).thenReturn(100);
//列印出"100"
System.out.println(list.size());
//充值mock, 之前的互動和stub将全部失效
reset(list);
//列印出"0"
System.out.println(list.size());
}
6.2.12 更多的注解
使用注解都需要預先進行配置,怎麼配置見6.2.7說明
- @Captor 替代ArgumentCaptor
- @Spy 替代spy(Object)
- @Mock 替代mock(Class)
- @InjectMocks 建立一個執行個體,其餘用@Mock(或@Spy)注解建立的mock将被注入到用該執行個體中
6.3 容易概念混淆的幾個點
6.3.1 @Mock與@Spy的異同
- Mock對象隻能調用stubbed方法,不能調用其真實的方法。而Spy對象可以監視一個真實的對象,對Spy對象進行方法調用時,會調用真實的方法。
- 兩者都可以stubbing對象的方法,讓方法傳回我們的期望值。
- 兩者無論是否是真實的方法調用,都可進行verify驗證。
- 對final類、匿名類、java的基本資料類型是無法進行mock或者spy的。
- 注意mockito是不能mock static方法的。
6.3.2 @InjectMocks與@Mock等的差別
@Mock:建立一個mock對象。
@InjectMocks:建立一個執行個體對象,然後将@Mcok或者@Spy注解建立的mock對象注入到該執行個體對象中。
stackoverflow上對這個有一個比較形象的解釋:
https://stackoverflow.com/questions/16467685/difference-between-mock-and-injectmocks@RunWith(MockitoJUnitRunner.class)
public class SomeManagerTest {
@InjectMocks
private SomeManager someManager;
@Mock
private SomeDependency someDependency; // 該mock對象會被注入到someManager對象中
//你不用向下面這樣執行個體化一個SomeManager對象,@InjectMocks會自動幫你實作
//SomeManager someManager = new SomeManager();
//SomeManager someManager = new SomeManager(someDependency);
}
6.3.3 when(...).thenReturn()與doReturn(...).when(...)兩種文法的異同
- 兩者都是用來stubbing方法的,大部分情況下,兩者可以表達同樣的意思,與Java裡的do/while、while/do語句類似。
- 對void方法不能使用when/thenReturn文法。
- 對spy對象要慎用when/thenReturn,如:
List spyList = spy(new ArrayList());
//下面代碼會抛出IndexOutOfBoundsException
when(spyList.get(0)).thenReturn("foo");
//這裡不會抛出異常
doReturn("foo").when(spyList).get(0);
System.out.println(spyList.get(0));
這段代碼運作會抛出異常,當調用when(spyList.get(0)).thenReturn("foo")時,會調用真實對象的get(0),由于list是空的是以會抛出IndexOutOfBoundsException異常。用doReturn/when文法則不會,因為它不會真實調用get(0)方法。
個人覺得讨論哪種文法好是沒有意義的,推薦使用doReturn/when文法,不管是mock還是spy對象都适用。
6.4 小結
本文主要介紹了mockito架構的使用方法,以及為什麼要使用mockito來進行單元測試。熟練掌握mockito的常用方法,對我們來寫單元測試來說絕對是事半功倍。
系列文章:
Android單元測試(一):前言 Android單元測試(二):什麼是單元測試 Android單元測試(三):測試難點及方案選擇 Android單元測試(四):JUnit介紹 Android單元測試(五):JUnit進階 Android單元測試(六):Mockito學習 Android單元測試(七):Robolectric介紹 Android單元測試(八):怎樣測試異步代碼