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 來進行測試。

入門

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

        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 的值。

打樁需要注意以下幾點

  • 對於 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。

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

        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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM