1. 程式人生 > >JUnit + Mockito 單元測試

JUnit + Mockito 單元測試

官方 ash nts exce hang value rev href argument

JUnit + Mockito 單元測試(二)

2015年01月05日 17:26:02 sp42a 閱讀數:60755 版權聲明:本文為博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/zhangxin09/article/details/42422643

JUnit 是單元測試框架。Mockito 與 JUnit 不同,並不是單元測試框架(這方面 JUnit 已經足夠好了),它是用於生成模擬對象或者直接點說,就是”假對象“的工具。兩者定位不同,所以一般通常的做法就是聯合 JUnit + Mockito 來進行測試。

入門

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

  1. List mock = mock( List.class );
  2. when( mock.get(0) ).thenReturn( 1 );
  3. assertEquals( "預期返回1", 1, mock.get( 0
    ) );// mock.get(0) 返回 1

其中 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 什麽方法,就要寫出相應的實現方法!

  1. HttpServletRequest request = mock(HttpServletRequest.class);
  2. when(request.getParameter("foo")).thenReturn("boo");

這裏“打樁”之後,我們執行 request.getParamter("foo") 就會返回 boo,如果不這樣設定,Mockito 就會返回默認的 null,也不會報錯說這個方法找不到。mock 實例默認的會給所有的方法添加基本實現:返回 null 或空集合,或者 0 等基本類型的值。這取決於方法返回類型,如 int 會返回 0,布爾值返回 false。對於其他 type 會返回 null。

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

  1. // 第一種方式
  2. when(i.next()).thenReturn("Hello").thenReturn("World");
  3. // 第二種方式
  4. when(i.next()).thenReturn("Hello", "World");
  5. // 第三種方式,都是等價的
  6. when(i.next()).thenReturn("Hello");
  7. when(i.next()).thenReturn("World");

第一次調用 i.next() 將返回 ”Hello”,第二次的調用會返回 ”World”。

上述我們一直在討論被測試的方法都有返回值的,那麽沒有返回值的 void 方法呢?也是測試嗎?答案是肯定的。——只不過 Mockito 要求你的寫法上有不同,因為都沒返回值了,調用 thenReturn(xxx) 肯定不行,取而代之的寫法是,

  1. doNothing().when(obj).notify();
  2. // 或直接
  3. when(obj).notify();

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

  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() 則意味著參數為任意值。例子如下,

  1. when(mockedList.get(anyInt())).thenReturn("element");
  2. System.out.println(mockedList.get(999));// 此時打印是 element

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

獲取返回的結果

一個問題,thenReturn 是返回結果是我們寫死的。如果要讓被測試的方法不寫死,返回實際結果並讓我們可以獲取到的——怎麽做呢?有時我們需要自定義方法執行的返回結果,Answer 接口就是滿足這樣的需求而存在的。

例如模擬常見的 request.getAttribute(key),由於這本來是個接口,所以連內部實現都要自己寫了。此次通過 Answer 接口獲取參數內容。

  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. when(request.getAttribute("isRawOutput")).thenReturn(true);
  9. when(request.getAttribute("errMsg")).thenAnswer(aswser);
  10. when(request.getAttribute("msg")).thenAnswer(aswser);

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

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

void 方法可以獲取參數,只是寫法上有區別,

  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(……) 屬於狀態測試,某些時候,測試不關心返回結果,而是側重方法有否被正確的參數調用過,這時候就應該使用 驗證方法了。從概念上講,就是和狀態測試所不同的“行為測試”了。

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

verify() 說明其作用的例子,

  1. mockedList.add("one");
  2. mockedList.add("two");
  3. verify(mockedList).add("one"); // 如果times不傳入,則默認是1

verify 內部跟蹤了所有的方法調用和參數的調用情況,然後會返回一個結果,說明是否通過。參見另外一個詳細的例子。

  1. Map mock = Mockito.mock( Map.class );
  2. when( mock.get( "city" ) ).thenReturn( "廣州" );
  3. // 關註參數有否傳入
  4. verify(mock).get( Matchers.eq( "city" ) );
  5. // 關註調用的次數
  6. verify(mock, 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。

  1. // correct
  2. verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));
  3. // will throw exception
  4. verify(mock).someMethod(anyInt(), anyString(), "third argument");

在最後的驗證時如果只輸入字符串”hello”是會報錯的,必須使用 Matchers 類內建的 eq 方法。

  1. Map mapMock = mock(Map.class);
  2. when(mapMock.put(anyInt(), anyString())).thenReturn("world");
  3. mapMock.put(1, "hello");
  4. verify(mapMock).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 有點類似。下面舉官方的例子來說明:

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

可以看到 spy 保留了 list 的大部分功能,只是將它的 size() 方法改寫了。不過 spy 在使用的時候有很多地方需要註意,一不小心就會導致問題,所以不到萬不得已還是不要用 spy。

總結例子

出處在這裏。

  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. // userService.save(user);
  16. user.setPassword("123456");
  17. String userId = userService.save(user);
  18. // 斷言返回結果
  19. assertThat(userId).isNotEmpty().hasSize(32);
  20. verify(mockUserDao, times(2)).findUserByLoginName(anyString());
  21. verify(mockUserDao).save(any(User.class));
  22. }
  23. @Test
  24. public void save2() {
  25. User user = new User();
  26. user.setLoginName("admin");
  27. user.setPassword("123456");
  28. userService.save(user);
  29. // 通過ArgumentCaptor(參數捕獲器) 對傳入參數進行驗證
  30. ArgumentCaptor<User> argument = ArgumentCaptor.forClass(User.class);
  31. verify(mockUserDao).save(argument.capture());
  32. assertThat("admin").isEqualTo(argument.getValue().getLoginName());
  33. // stub 調用save方法時拋出異常
  34. doThrow(new ServiceException("測試拋出異常")).when(mockUserDao).save(any(User.class));
  35. try {
  36. userService.save(user);
  37. failBecauseExceptionWasNotThrown(RuntimeException.class);
  38. } catch (ServiceException se) {
  39. }
  40. }

其他高級話題

如果沒有 JUnit,可以使用 Mockito 的 @Before 的註解,進行一些前期的初始化工作,

  1. public class ArticleManagerTest {
  2. @Mock private ArticleCalculator calculator;
  3. @Mock private ArticleDatabase database;
  4. @Mock private UserProvider userProvider;
  5. @Before public void setup() {
  6. MockitoAnnotations.initMocks(testClass);
  7. }
  8. }

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

  1. @RunWith(MockitoJUnitRunner.class)
  2. public class ExampleTest {
  3. @Mock private List list;
  4. @Test public void shouldDoSomething() {
  5. list.add(100);
  6. }
  7. }

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

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

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

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

參見資源:

  • Mockito 專欄博客
  • Mockito(二)--實例篇

JUnit + Mockito 單元測試