JUnit + Mockito 單元測試(二)(good)

 

import org.junit.Test;
import org.mockito.Matchers;
import org.mockito.Mockito;

import java.util.List;
import java.util.Map;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Mockito.*;

/**
 * Created by MyWorld on 2016/1/26.
 */
public class MockitoDemo {

    @Test
    public void mockitoMapDemo1() {
        Map mockedMap = Mockito.mock(Map.class);
        when(mockedMap.get("city")).thenReturn("廣州");
        Object cityValue = mockedMap.get("city");
        assertThat(cityValue.toString(), is("廣州"));
        verify(mockedMap).get(Matchers.eq("city"));
        verify(mockedMap, times(2));
    }

    @Test
    public void mockitoMapDemo2() {
        Map mockedMap = Mockito.mock(Map.class);
//        when(mockedMap.put(anyInt(), anyString())).thenReturn("world");
        mockedMap.put(1, "hello");
        verify(mockedMap).put(anyInt(), eq("hello"));
    }

    @Test
    public void mockitoListDemo() {
        List mockedList = Mockito.mock(List.class);
        mockedList.add("one");
        mockedList.add("two");
        verify(mockedList).add("one");
        verify(mockedList, times(2)).add(anyString());
    }
}

 

JUnit 是單元測試框架。Mockito 與 JUnit 不一樣,並非單元測試框架(這方面 JUnit 已經足夠好了),它是用於生成模擬對象或者直接點說,就是」假對象「的工具。二者定位不一樣,因此通常一般的作法就是聯合 JUnit + Mockito 來進行測試。html

入門

首先是配置 Mock 對象,看看例子怎麼寫的。java

        List mockedList = Mockito.mock(List.class);
        when(mockedList.get(0)).thenReturn(1);
        assertEquals("Descriptive information ", 1, mockedList.get(0));

其中 mock 是模擬 List 的對象,擁有 List 的全部方法和屬性。when(xxxx).thenReturn(yyyy); 是指定當執行了這個方法的時候,返回 thenReturn 的值,至關因而對模擬對象的配置過程,爲某些條件給定一個預期的返回值。相信經過這個簡單的例子你能夠明白所謂 Mock 即是這麼一回事。正則表達式

咱們看到 List 爲 Java.util.List 是接口,並非實現類,但這不妨礙咱們使用它做爲咱們的「打樁」對象,——固然你也可使用實現類,傳入 mock(obj) 方法中。
這裏提到的是"打樁(Stub,也有人稱其爲「存根」)"的概念,是一個形象的說法,就是把所需的測試數據塞進對象中,
適用於基於狀態的(state-based)測試關注的是輸入和輸出
Mockito 中 when(…).thenReturn(…)  這樣的語法來定義對象方法和參數(輸入),而後在 thenReturn 中指定結果(輸出)。此過程稱爲 Stub 打樁。
一旦這個方法被 stub 了,就會一直返回這個 stub 的值。api

打樁須要注意如下幾點數組

  • 對於 static 和 final 方法, Mockito 沒法對其 when(…).thenReturn(…) 操做。
  • 當咱們連續兩次爲同一個方法使用 stub 的時候,他只會只用最新的一次。

mock 對象會覆蓋整個被 mock 的對象,所以沒有 stub 的方法只能返回默認值。又由於,咱們 mock 一個接口的時候,不少成員方法只是一個簽名,並無實現,這就要咱們手動寫出這些實現方法啦。典型地,咱們模擬一個 request 請求對象,你被測試的代碼中使用了 HttpSerevletRequest 什麼方法,就要寫出相應的實現方法!框架

        HttpServletRequest mockedRequest = Mockito.mock(HttpServletRequest.class);
        when(mockedRequest.getParameter("foo")).thenReturn("too");
        assertThat(mockedRequest.getParameter("foo"), is("too"));

這裏「打樁」以後,咱們執行 request.getParamter("foo") 就會返回 boo,若是不這樣設定,Mockito 就會返回默認的 null,也不會報錯說這個方法找不到。
mock 實例默認的會給全部的方法添加基本實現:返回 null 或空集合,或者 0 等基本類型的值。
這取決於方法返回類型,如 int 會返回 0,布爾值返回 false。對於其餘 type 會返回 null。ide

打樁支持迭代風格的返回值設定,例如,工具

        Iterator mockedIterator = Mockito.mock(Iterator.class);
        //第一種方式
        when(mockedIterator.next()).thenReturn("hello").thenReturn("world");
        //第二種方式
        when(mockedIterator.next()).thenReturn("hello", "world");
        //第三種方式
        when(mockedIterator.next()).thenReturn("hello");
        when(mockedIterator.next()).thenReturn("world");

會返回 」World」。單元測試

上述咱們一直在討論被測試的方法都有返回值的,那麼沒有返回值的 void 方法呢?也是測試嗎?答案是確定的。——只不過 Mockito 要求你的寫法上有不一樣,由於都沒返回值了,調用 thenReturn(xxx) 確定不行,取而代之的寫法是,學習

[java]  view plain  copy
 
  1. doNothing().when(obj).notify();  
  2. // 或直接  
  3. when(obj).notify();  

Mockito 還能對被測試的方法強行拋出異常,

[java]  view plain  copy
 
  1. when(i.next()).thenThrow(new RuntimeException());  
  2. doThrow(new RuntimeException()).when(i).remove(); // void 方法的  
  3. // 迭代風格   
  4. doNothing().doThrow(new RuntimeException()).when(i).remove(); // 第一次調用 remove 方法什麼都不作,第二次調用拋出 RuntimeException 異常。  

如需指定異常類型,參見這裏

模擬傳入的參數 argument matchers

拿上面的例子說,其中一個問題,

when(request.getParameter( "foo")).thenReturn("boo");  

這裏 getParameter("foo") 這裏咱們是寫死參數 foo 的,可是若是我不關心輸入的具體內容,能夠嗎?能夠的,最好能像正則表達式那樣,/w+ 表示任意字符串是否是很方便,不用考慮具體什麼參數,只要是 字符串 型的參數,就能夠打樁。如此方便的想法 Mockito 也考慮到了,提供 argument matchers 機制,例如 anyString() 匹配任何 String 參數,anyInt() 匹配任何 int 參數,anySet() 匹配任何 Set,any() 則意味着參數爲任意值。例子以下,

when(mockedList.get(anyInt())).thenReturn("element");     

System.out.println(mockedList.get(999));// 此時打印是 element     

再進一步,自定義類型也能夠,如 any(User.class),另,參見《學習 Mockito - 自定義參數匹配器》 和 這裏 和 這裏

獲取返回的結果

一個問題,thenReturn 是返回結果是咱們寫死的。若是要讓被測試的方法不寫死,返回實際結果並讓咱們能夠獲取到的——怎麼作呢?
有時咱們須要自定義方法執行的返回結果,Answer 接口就是知足這樣的需求而存在的。

例如模擬常見的 request.getAttribute(key),因爲這原本是個接口,因此連內部實現都要本身寫了。這次經過 Answer 接口獲取參數內容。

[java]  view plain  copy
 
  1. final Map<String, Object> hash = new HashMap<String, Object>();  
  2. Answer<String> aswser = new Answer<String>() {    
  3.     public String answer(InvocationOnMock invocation) {    
  4.         Object[] args = invocation.getArguments();    
  5.         return hash.get(args[0].toString()).toString();    
  6.     }   
  7. };  
  8.   
  9. when(request.getAttribute("isRawOutput")).thenReturn(true);   
  10. when(request.getAttribute("errMsg")).thenAnswer(aswser);   
  11. when(request.getAttribute("msg")).thenAnswer(aswser);  

利用 InvocationOnMock 提供的方法能夠獲取 mock 方法的調用信息。下面是它提供的方法:

  • getArguments() 調用後會以 Object 數組的方式返回 mock 方法調用的參數。
  • getMethod() 返回 java.lang.reflect.Method 對象
  • getMock() 返回 mock 對象
  • callRealMethod() 真實方法調用,若是 mock 的是接口它將會拋出異常

void 方法能夠獲取參數,只是寫法上有區別,

[java]  view plain  copy
 
  1. doAnswer(new Answer<Object>() {  
  2.     public Object answer(InvocationOnMock invocation) {  
  3.         Object[] args = invocation.getArguments();  
  4.         // Object mock = invocation.getMock();    
  5.         System.out.println(args[1]);  
  6.         hash.put(args[0].toString(), args[1]);  
  7.         return "called with arguments: " + args;  
  8.     }  
  9. }).when(request).setAttribute(anyString(), anyString());  

其實就是一個回調,——若是不是接口,是實現類的話,估計不用本身寫實現。

驗證 Verify

前面提到的 when(……).thenReturn(……) 屬於狀態測試,某些時候,測試不關心返回結果,而是側重方法有否被正確的參數調用過,這時候就應該使用 驗證方法了。從概念上講,就是和狀態測試所不一樣的「行爲測試」了。

一旦使用 org.mockito.Mockito.mock() 對模擬對象打樁,意味着 Mockito 會記錄着這個模擬對象調用了什麼方法,還有調用了多少次。
最後由用戶決定是否須要進行驗證,即 org.mockito.Mockito.verify() 方法。

verify() 說明其做用的例子:

        List mockedList = Mockito.mock(List.class);
        mockedList.add("one");
        mockedList.add("two");
        verify(mockedList).add("one");// 若是times不傳入,則默認是1
        verify(mockedList, times(2)).add(anyString());

 

verify 內部跟蹤了全部的方法調用和參數的調用狀況,而後會返回一個結果,說明是否經過。參見另一個詳細的例子。

 

        Map mockedMap = Mockito.mock(Map.class);
        when(mockedMap.get("city")).thenReturn("廣州");
        Object cityValue = mockedMap.get("city");
        assertThat(cityValue.toString(), is("廣州"));
        // 關注參數有否傳入  
        verify(mockedMap).get(Matchers.eq("city"));
        // 關注調用的次數  
        verify(mockedMap, times(2));

也就是說,這是對歷史記錄做一種回溯校驗的處理。

這裏補充一個學究的問題,所謂 Mock 與 Stub 打樁,其實它們之間不能互爲其表。但 Mockito 語境中則 Stub 和 Mock 對象同時使用的。由於它既能夠設置方法調用返回值,又能夠驗證方法的調用。有關 stub 和 mock 的詳細論述請見 Martin Fowler 大叔的文章《Mocks Aren't Stub》

Mockito 除了提供 times(N) 方法供咱們調用外,還提供了不少可選的方法:

  • never() 沒有被調用,至關於 times(0)
  • atLeast(N) 至少被調用 N 次
  • atLeastOnce() 至關於 atLeast(1)
  • atMost(N) 最多被調用 N 次

verify 也能夠像 when 那樣使用模擬參數,若方法中的某一個參數使用了matcher,則全部的參數都必須使用 matcher

        Map mockedMap = Mockito.mock(Map.class);
        mockedMap.put("", "");
        String newValue = "newValue";
        String oldValue = "oldValue";
        //若方法中的某一個參數使用了matcher,則全部的參數都必須使用 matcher.
        // 不然會報異常:org.mockito.exceptions.misusing.InvalidUseOfMatchersException:
        // When using matchers, all arguments have to be provided by matchers.
        when(mockedMap.put(anyInt(), eq(newValue))).thenReturn(oldValue);//mock a put operation
        Object oldValueForPut = mockedMap.put(1, newValue);//get the mock value
        assertThat(oldValueForPut.toString(), is(oldValue));//assert the mock operation

        verify(mockedMap).put(anyInt(), eq(newValue));//verify  whether the mock operation execute or not

        mockedMap.put(1, "hello");
        verify(mockedMap).put(anyInt(), eq("hello"));

 

其餘高級用法,詳見《學習 Mockito - Mock對象的行爲驗證》,主要特性以下,

  • 參數驗證,詳見《利用 ArgumentCaptor(參數捕獲器)捕獲方法參數進行驗證》
  • 超時驗證,經過 timeout,並制定毫秒數驗證超時。注意,若是被調用屢次,times 仍是須要的。
  • 方法調用順序 經過 InOrder 對象,驗證方法的執行順序,如上例子中,若是 mock 的 get(0) 和 get(1) 方法反過來則測試不經過。這裏 mock2 其實沒有被調用過。因此不須要些。
  • verifyNoMoreInteractions 查詢是否存在被調用,但未被驗證的方法,若是存在則拋出異常。這裏由於驗證了get(anyInt()),至關於全部的 get 方法被驗證,因此經過。
  • verifyZeroInteractions 查詢對象是否未產生交互,若是傳入 的 mock 對象的方法被調用過,則拋出異常。這裏 mock2 的方法沒有被調用過,全部經過。

參見《用mockito的verify來驗證mock的方法是否被調用》

看mockito的api時,一直都不清楚veriry()這個方法的做用,由於若是我mock了某個方法,確定是爲了調用的啊。直到今天在迴歸接口測試用例的時候,發現有兩個用例,用例2比用例1多了一個 mock 的步驟,不過最後的結果輸出是同樣的。因爲代碼作了修改,我從新 mock 後,其實用例2中對於的步驟是不會執行的,可測試仍是經過了。仔細查看後,發現mock的方法沒有被調用,因此用例2和用例1就變成同樣的了。因而,就產生了這麼個需求:單單經過結果來判斷正確與否仍是不夠的,我還要判斷是否按我指定的路徑執行的用例。到這裏,終於領略到了mockito的verify的強大威力,如下是示例代碼:

若調用成功,則程序正常運行,反之則會報告: Wanted but not invoked:verify(mockedList).add("one"); 錯誤。

感受 verify 會用的比較少。

Spy

spy 的意思是你能夠修改某個真實對象的某些方法的行爲特徵,而不改變他的基本行爲特徵,這種策略的使用跟 AOP 有點相似。下面舉官方的例子來講明:

[java]  view plain  copy
 
  1. List list = new LinkedList();    
  2. List spy = spy(list);    
  3.     
  4. //optionally, you can stub out some methods:    
  5. when(spy.size()).thenReturn(100);    
  6.      
  7. //using the spy calls <b>real</b> methods    
  8. spy.add("one");    
  9. spy.add("two");    
  10.      
  11. //prints "one" - the first element of a list    
  12. System.out.println(spy.get(0));    
  13.      
  14. //size() method was stubbed - 100 is printed    
  15. System.out.println(spy.size());    
  16.      
  17. //optionally, you can verify    
  18. verify(spy).add("one");    
  19. verify(spy).add("two");  

能夠看到 spy 保留了 list 的大部分功能,只是將它的 size() 方法改寫了。不過 spy 在使用的時候有不少地方須要注意,一不當心就會致使問題,因此不到萬不得已仍是不要用 spy。

總結例子

出處在這裏

 

[java]  view plain  copy
 
  1. @Test    
  2. public void save() {    
  3.     User user = new User();    
  4.     user.setLoginName("admin");    
  5.     // 第一次調用findUserByLoginName返回user 第二次調用返回null    
  6.     when(mockUserDao.findUserByLoginName(anyString())).thenReturn(user).thenReturn(null);    
  7.     try {    
  8.         // 測試若是重名會拋出異常    
  9.         userService.save(user);    
  10.         // 若是沒有拋出異常測試不經過    
  11.         failBecauseExceptionWasNotThrown(RuntimeException.class);    
  12.     } catch (ServiceException se) {    
  13.     }    
  14.     verify(mockUserDao).findUserByLoginName("admin");    
  15.     
  16.     // userService.save(user);    
  17.     user.setPassword("123456");    
  18.     String userId = userService.save(user);    
  19.     // 斷言返回結果    
  20.     assertThat(userId).isNotEmpty().hasSize(32);    
  21.     
  22.     verify(mockUserDao, times(2)).findUserByLoginName(anyString());    
  23.     verify(mockUserDao).save(any(User.class));    
  24. }    
  25.     
  26. @Test    
  27. public void save2() {    
  28.     User user = new User();    
  29.     user.setLoginName("admin");    
  30.     user.setPassword("123456");    
  31.     userService.save(user);    
  32.     
  33.     // 經過ArgumentCaptor(參數捕獲器) 對傳入參數進行驗證    
  34.     ArgumentCaptor<User> argument = ArgumentCaptor.forClass(User.class);    
  35.     verify(mockUserDao).save(argument.capture());    
  36.     assertThat("admin").isEqualTo(argument.getValue().getLoginName());    
  37.     
  38.     // stub 調用save方法時拋出異常    
  39.     doThrow(new ServiceException("測試拋出異常")).when(mockUserDao).save(any(User.class));    
  40.     try {    
  41.         userService.save(user);    
  42.         failBecauseExceptionWasNotThrown(RuntimeException.class);    
  43.     } catch (ServiceException se) {    
  44.     }    
  45. }    

 

其餘高級話題

若是沒有 JUnit,可使用 Mockito 的 @Before 的註解,進行一些前期的初始化工做,

[java]  view plain  copy
 
 在CODE上查看代碼片派生到個人代碼片
  1. public class ArticleManagerTest {  
  2.     @Mock 
  3.       private ArticleCalculator calculator;  
  4.     @Mock 
  5.     private ArticleDatabase database;  
  6.     @Mock 
  7.     private UserProvider userProvider;  
  8.   
  9.     @Before 
  10.      public void setup() {  
  11.         MockitoAnnotations.initMocks(testClass);  
  12.     }  
  13. }   

若是有 JUnit,則無需 @Before,但要修改 JUnit 默認容器,

[java]  view plain  copy
 
 在CODE上查看代碼片派生到個人代碼片
  1. @RunWith(MockitoJUnitRunner.class)  
  2. public class ExampleTest {  
  3.     @Mock 
        private List list;  
  4.   
  5.     @Test public void shouldDoSomething() {  
  6.         list.add(100);  
  7.     }  
  8. }  

在 JUnit 中有不少個 Runner ,他們負責調用你的測試代碼,每個 Runner 都有各自的特殊功能,你要根據須要選擇不一樣的 Runner 來運行你的測試代碼。

----------------------------------------------------

貌似 Mockito 的註解都比較強大,有待之後再看看:

《學習Mockito - Mockito對Annotation的支持》, http://jilen.iteye.com/blog/1427368

參見資源:

 

1自動生成Mock類

在須要Mock的屬性上標記@Mock註解,而後@RunWith(MockitoJUnitRunner.class)或者在setUp()方法中顯示調用MockitoAnnotations.initMocks(this);生成Mock類便可。

3 Mock方法定製不再用錄製、播放了

Mockito的Mock方法定製可讀性很強,並且也不須要像EasyMock那樣錄製播放,定製後就可使用。
例如:
when(userDao.selectAll()).
thenReturn(Collections.<UserDomain>emptyList());

http://blog.csdn.net/dc_726/article/details/8568537

相關文章
相關標籤/搜索