Mockito教程
2017-01-20
1 Mockito 介紹
1.1 Mockito是什么?
1.2 為什么需要Mock
1.3 Stub和Mock異同
1.4 Mockito資源
1.5 使用場景
2 使用Mockito
2.1 驗證行為
2.2 模擬我們所期望的結果
2.3 RETURNS_SMART_NULLS和RETURNS_DEEP_STUBS
2.4 模擬方法體拋出異常
2.5 使用注解來快速模擬
2.6 參數匹配
2.7 自定義參數匹配
2.8 捕獲參數來進一步斷言
2.9 使用方法預期回調接口生成期望值(Answer結構)
2.10 修改對未預設的調用返回默認期望
2.11 用spy監控真實對象
2.12 真實的部分mock
2.13 重置mock
2.14 驗證確切的調用次數
2.15 連續調用
2.16 驗證執行順序
2.17 確保模擬對象上無互動發生
2.18 找出冗余的互動(即未被驗證到的)
3 Mockito如何實現Mock
參考
1 Mockito 介紹 [3]
1.1 Mockito是什么?
Mockito是mocking框架,它讓你用簡潔的API做測試。而且Mockito簡單易學,它可讀性強和驗證語法簡潔。
1.2 為什么需要Mock
測試驅動的開發( TDD)要求我們先寫單元測試,再寫實現代碼。在寫單元測試的過程中,我們往往會遇到要測試的類有很多依賴,這些依賴的類/對象/資源又有別的依賴,從而形成一個大的依賴樹,要在單元測試的環境中完整地構建這樣的依賴,是一件很困難的事情。如下圖所示:
為了測試類A,我們需要Mock B類和C類(用虛擬對象來代替)如下圖所示:
1.3 Stub和Mock異同[1]
- 相同:Stub和Mock都是模擬外部依賴
- 不同:Stub是完全模擬一個外部依賴, 而Mock還可以用來判斷測試通過還是失敗
1.4 Mockito資源
- 官網: http://mockito.org
- API文檔:http://docs.mockito.googlecode.com/hg/org/mockito/Mockito.html
- 項目源碼:https://github.com/mockito/mockito
1.5 使用場景
- 提前創建測試; TDD(測試驅動開發)
- 團隊可以並行工作
- 你可以創建一個驗證或者演示程序
- 為無法訪問的資源編寫測試
- Mock 可以交給用戶
- 隔離系統
2 使用Mockito [2][4]
添加maven依賴
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-all</artifactId> <version>1.9.5</version> <scope>test</scope> </dependency>
添加junit依賴
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency>
添加引用
import static org.mockito.Mockito.*; import static org.junit.Assert.*;
2.1 驗證行為
@Test public void verify_behaviour(){ //模擬創建一個List對象 List mock = mock(List.class); //使用mock的對象 mock.add(1); mock.clear(); //驗證add(1)和clear()行為是否發生 verify(mock).add(1); verify(mock).clear(); }
2.2 模擬我們所期望的結果
@Test public void when_thenReturn(){ //mock一個Iterator類 Iterator iterator = mock(Iterator.class); //預設當iterator調用next()時第一次返回hello,第n次都返回world when(iterator.next()).thenReturn("hello").thenReturn("world"); //使用mock的對象 String result = iterator.next() + " " + iterator.next() + " " + iterator.next(); //驗證結果 assertEquals("hello world world",result); }
@Test(expected = IOException.class) public void when_thenThrow() throws IOException { OutputStream outputStream = mock(OutputStream.class); OutputStreamWriter writer = new OutputStreamWriter(outputStream); //預設當流關閉時拋出異常 doThrow(new IOException()).when(outputStream).close(); outputStream.close(); }
2.3 RETURNS_SMART_NULLS和RETURNS_DEEP_STUBS
RETURNS_SMART_NULLS實現了Answer接口的對象,它是創建mock對象時的一個可選參數,mock(Class,Answer)。
在創建mock對象時,有的方法我們沒有進行stubbing,所以調用時會放回Null這樣在進行操作是很可能拋出NullPointerException。如果通過RETURNS_SMART_NULLS參數創建的mock對象在沒有調用stubbed方法時會返回SmartNull。例如:返回類型是String,會返回"";是int,會返回0;是List,會返回空的List。另外,在控制台窗口中可以看到SmartNull的友好提示。
@Test
public void returnsSmartNullsTest() { List mock = mock(List.class, RETURNS_SMART_NULLS); System.out.println(mock.get(0)); //使用RETURNS_SMART_NULLS參數創建的mock對象,不會拋出NullPointerException異常。另外控制台窗口會提示信息“SmartNull returned by unstubbed get() method on mock” System.out.println(mock.toArray().length); }
RETURNS_DEEP_STUBS也是創建mock對象時的備選參數
RETURNS_DEEP_STUBS參數程序會自動進行mock所需的對象,方法deepstubsTest和deepstubsTest2是等價的
@Test
public void deepstubsTest(){ Account account=mock(Account.class,RETURNS_DEEP_STUBS); when(account.getRailwayTicket().getDestination()).thenReturn("Beijing"); account.getRailwayTicket().getDestination(); verify(account.getRailwayTicket()).getDestination(); assertEquals("Beijing",account.getRailwayTicket().getDestination()); } @Test public void deepstubsTest2(){ Account account=mock(Account.class); RailwayTicket railwayTicket=mock(RailwayTicket.class); when(account.getRailwayTicket()).thenReturn(railwayTicket); when(railwayTicket.getDestination()).thenReturn("Beijing"); account.getRailwayTicket().getDestination(); verify(account.getRailwayTicket()).getDestination(); assertEquals("Beijing",account.getRailwayTicket().getDestination()); } public class RailwayTicket{ private String destination; public String getDestination() { return destination; } public void setDestination(String destination) { this.destination = destination; } } public class Account{ private RailwayTicket railwayTicket; public RailwayTicket getRailwayTicket() { return railwayTicket; } public void setRailwayTicket(RailwayTicket railwayTicket) { this.railwayTicket = railwayTicket; } }
2.4 模擬方法體拋出異常
@Test(expected = RuntimeException.class) public void doThrow_when(){ List list = mock(List.class); doThrow(new RuntimeException()).when(list).add(1); list.add(1); }
2.5 使用注解來快速模擬
在上面的測試中我們在每個測試方法里都mock了一個List對象,為了避免重復的mock,是測試類更具有可讀性,我們可以使用下面的注解方式來快速模擬對象:
@Mock
private List mockList;
OK,我們再用注解的mock對象試試
@Test
public void shorthand(){ mockList.add(1); verify(mockList).add(1); }
運行這個測試類你會發現報錯了,mock的對象為NULL,為此我們必須在基類中添加初始化mock的代碼
public class MockitoExample2 { @Mock private List mockList; public MockitoExample2(){ MockitoAnnotations.initMocks(this); } @Test public void shorthand(){ mockList.add(1); verify(mockList).add(1); } }
或者使用built-in runner:MockitoJUnitRunner
@RunWith(MockitoJUnitRunner.class) public class MockitoExample2 { @Mock private List mockList; @Test public void shorthand(){ mockList.add(1); verify(mockList).add(1); } }
2.6 參數匹配
@Test public void with_arguments(){ Comparable comparable = mock(Comparable.class); //預設根據不同的參數返回不同的結果 when(comparable.compareTo("Test")).thenReturn(1); when(comparable.compareTo("Omg")).thenReturn(2); assertEquals(1, comparable.compareTo("Test")); assertEquals(2, comparable.compareTo("Omg")); //對於沒有預設的情況會返回默認值 assertEquals(0, comparable.compareTo("Not stub")); }
除了匹配制定參數外,還可以匹配自己想要的任意參數
@Test public void with_unspecified_arguments(){ List list = mock(List.class); //匹配任意參數 when(list.get(anyInt())).thenReturn(1); when(list.contains(argThat(new IsValid()))).thenReturn(true); assertEquals(1, list.get(1)); assertEquals(1, list.get(999)); assertTrue(list.contains(1)); assertTrue(!list.contains(3)); } private class IsValid extends ArgumentMatcher<List>{ @Override public boolean matches(Object o) { return o == 1 || o == 2; } }
注意:如果你使用了參數匹配,那么所有的參數都必須通過matchers來匹配,如下代碼:
@Test public void all_arguments_provided_by_matchers(){ Comparator comparator = mock(Comparator.class); comparator.compare("nihao","hello"); //如果你使用了參數匹配,那么所有的參數都必須通過matchers來匹配 verify(comparator).compare(anyString(),eq("hello")); //下面的為無效的參數匹配使用 //verify(comparator).compare(anyString(),"hello"); }
2.7 自定義參數匹配
@Test public void argumentMatchersTest(){ //創建mock對象 List<String> mock = mock(List.class); //argThat(Matches<T> matcher)方法用來應用自定義的規則,可以傳入任何實現Matcher接口的實現類。 when(mock.addAll(argThat(new IsListofTwoElements()))).thenReturn(true); mock.addAll(Arrays.asList("one","two","three")); //IsListofTwoElements用來匹配size為2的List,因為例子傳入List為三個元素,所以此時將失敗。 verify(mock).addAll(argThat(new IsListofTwoElements())); } class IsListofTwoElements extends ArgumentMatcher<List> { public boolean matches(Object list) { return((List)list).size()==2; } }
2.8 捕獲參數來進一步斷言
較復雜的參數匹配器會降低代碼的可讀性,有些地方使用參數捕獲器更加合適。
@Test
public void capturing_args(){ PersonDao personDao = mock(PersonDao.class); PersonService personService = new PersonService(personDao); ArgumentCaptor<Person> argument = ArgumentCaptor.forClass(Person.class); personService.update(1,"jack"); verify(personDao).update(argument.capture()); assertEquals(1,argument.getValue().getId()); assertEquals("jack",argument.getValue().getName()); } class Person{ private int id; private String name; Person(int id, String name) { this.id = id; this.name = name; } public int getId() { return id; } public String getName() { return name; } } interface PersonDao{ public void update(Person person); } class PersonService{ private PersonDao personDao; PersonService(PersonDao personDao) { this.personDao = personDao; } public void update(int id,String name){ personDao.update(new Person(id,name)); } }
2.9 使用方法預期回調接口生成期望值(Answer結構)
@Test
public void answerTest(){ when(mockList.get(anyInt())).thenAnswer(new CustomAnswer()); assertEquals("hello world:0",mockList.get(0)); assertEquals("hello world:999",mockList.get(999)); } private class CustomAnswer implements Answer<String>{ @Override public String answer(InvocationOnMock invocation) throws Throwable { Object[] args = invocation.getArguments(); return "hello world:"+args[0]; } }
也可使用匿名內部類實現
@Test
public void answer_with_callback(){ //使用Answer來生成我們我們期望的返回 when(mockList.get(anyInt())).thenAnswer(new Answer<Object>() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { Object[] args = invocation.getArguments(); return "hello world:"+args[0]; } }); assertEquals("hello world:0",mockList.get(0)); assertEquals("hello world:999",mockList.get(999)); }
2.10 修改對未預設的調用返回默認期望
@Test
public void unstubbed_invocations(){ //mock對象使用Answer來對未預設的調用返回默認期望值 List mock = mock(List.class,new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { return 999; } }); //下面的get(1)沒有預設,通常情況下會返回NULL,但是使用了Answer改變了默認期望值 assertEquals(999, mock.get(1)); //下面的size()沒有預設,通常情況下會返回0,但是使用了Answer改變了默認期望值 assertEquals(999,mock.size()); }
2.11 用spy監控真實對象
- Mock不是真實的對象,它只是用類型的class創建了一個虛擬對象,並可以設置對象行為
- Spy是一個真實的對象,但它可以設置對象行為
- InjectMocks創建這個類的對象並自動將標記@Mock、@Spy等注解的屬性值注入到這個中
@Test(expected = IndexOutOfBoundsException.class) public void spy_on_real_objects(){ List list = new LinkedList(); List spy = spy(list); //下面預設的spy.get(0)會報錯,因為會調用真實對象的get(0),所以會拋出越界異常 //when(spy.get(0)).thenReturn(3); //使用doReturn-when可以避免when-thenReturn調用真實對象api doReturn(999).when(spy).get(999); //預設size()期望值 when(spy.size()).thenReturn(100); //調用真實對象的api spy.add(1); spy.add(2); assertEquals(100,spy.size()); assertEquals(1,spy.get(0)); assertEquals(2,spy.get(1)); verify(spy).add(1); verify(spy).add(2); assertEquals(999,spy.get(999)); spy.get(2); }
2.12 真實的部分mock
@Test
public void real_partial_mock(){ //通過spy來調用真實的api List list = spy(new ArrayList()); assertEquals(0,list.size()); A a = mock(A.class); //通過thenCallRealMethod來調用真實的api when(a.doSomething(anyInt())).thenCallRealMethod(); assertEquals(999,a.doSomething(999)); } class A{ public int doSomething(int i){ return i; } }
2.13 重置mock
@Test
public void reset_mock(){ List list = mock(List.class); when(list.size()).thenReturn(10); list.add(1); assertEquals(10,list.size()); //重置mock,清除所有的互動和預設 reset(list); assertEquals(0,list.size()); }
2.14 驗證確切的調用次數
@Test public void verifying_number_of_invocations(){ List list = mock(List.class); list.add(1); list.add(2); list.add(2); list.add(3); list.add(3); list.add(3); //驗證是否被調用一次,等效於下面的times(1) verify(list).add(1); verify(list,times(1)).add(1); //驗證是否被調用2次 verify(list,times(2)).add(2); //驗證是否被調用3次 verify(list,times(3)).add(3); //驗證是否從未被調用過 verify(list,never()).add(4); //驗證至少調用一次 verify(list,atLeastOnce()).add(1); //驗證至少調用2次 verify(list,atLeast(2)).add(2); //驗證至多調用3次 verify(list,atMost(3)).add(3); }
2.15 連續調用
@Test(expected = RuntimeException.class) public void consecutive_calls(){ //模擬連續調用返回期望值,如果分開,則只有最后一個有效 when(mockList.get(0)).thenReturn(0); when(mockList.get(0)).thenReturn(1); when(mockList.get(0)).thenReturn(2); when(mockList.get(1)).thenReturn(0).thenReturn(1).thenThrow(new RuntimeException()); assertEquals(2,mockList.get(0)); assertEquals(2,mockList.get(0)); assertEquals(0,mockList.get(1)); assertEquals(1,mockList.get(1)); //第三次或更多調用都會拋出異常 mockList.get(1); }
2.16 驗證執行順序
@Test public void verification_in_order(){ List list = mock(List.class); List list2 = mock(List.class); list.add(1); list2.add("hello"); list.add(2); list2.add("world"); //將需要排序的mock對象放入InOrder InOrder inOrder = inOrder(list,list2); //下面的代碼不能顛倒順序,驗證執行順序 inOrder.verify(list).add(1); inOrder.verify(list2).add("hello"); inOrder.verify(list).add(2); inOrder.verify(list2).add("world"); }
2.17 確保模擬對象上無互動發生
@Test public void verify_interaction(){ List list = mock(List.class); List list2 = mock(List.class); List list3 = mock(List.class); list.add(1); verify(list).add(1); verify(list,never()).add(2); //驗證零互動行為 verifyZeroInteractions(list2,list3); }
2.18 找出冗余的互動(即未被驗證到的)
@Test(expected = NoInteractionsWanted.class) public void find_redundant_interaction(){ List list = mock(List.class); list.add(1); list.add(2); verify(list,times(2)).add(anyInt()); //檢查是否有未被驗證的互動行為,因為add(1)和add(2)都會被上面的anyInt()驗證到,所以下面的代碼會通過 verifyNoMoreInteractions(list); List list2 = mock(List.class); list2.add(1); list2.add(2); verify(list2).add(1); //檢查是否有未被驗證的互動行為,因為add(2)沒有被驗證,所以下面的代碼會失敗拋出異常 verifyNoMoreInteractions(list2); }
3 Mockito如何實現Mock[3]
Mockito並不是創建一個真實的對象,而是模擬這個對象,他用簡單的when(mock.method(params)).thenRetrun(result)語句設置mock對象的行為,如下語句:
// 設置mock對象的行為 - 當調用其get方法獲取第0個元素時,返回"first" Mockito.when(mockedList.get(0)).thenReturn("first");
在Mock對象的時候,創建一個proxy對象,保存被調用的方法名(get),以及調用時候傳遞的參數(0),然后在調用thenReturn方法時再把“first”保存起來,這樣,就有了構建一個stub方法所需的所有信息,構建一個stub。當get方法被調用的時候,實際上調用的是之前保存的proxy對象的get方法,返回之前保存的數據。
參考
[1] 單元測試之Stub和Mock
[2] mockito簡單教程
[3] Mockito入門
[4] 學習Mockito