Mockito初探——快速入門

Mockito是基於CGLIB代理,實現打樁。它通過攔截對象的所有操作方法,對於滿足打樁條件的調用,返回預設的返回值。

主要註解

@InjectMocks

用於標記對象屬性允許用mock或spy注入。嘗試通過按「先構造函數注入再setter注入最後屬性(字段)注入」的順序注入依賴。

  • 構造函數注入:選取最大的構造函數,用已聲明的mock作爲參數注入;注:如果已經通過構造注入,將不再嘗試其他策略注入(即不會再有如下兩種的注入)

  • setter注入:先通過類型匹配,如果匹配的類型有多個mock實例,再通過名字匹配;

  • 字段注入:同setter注入(不同的是字段可以不提供setter方法),先類型再名字;

@Mock

用於標記一個Mock字段,被標記的對象將被創建爲mock對象(對於沒有打樁的方法,返回值爲默認值0或null或false);

  • 允許多次代理mock對象的同一個方法,但具體的行爲取決於該方法最近的一次代理行爲
  • mock對象的代理方法,允許多次調用,只要不覆蓋它的代理行爲,那麼每次調用的執行相同的行爲或者返回相同的值
  • 相同的方法和參數唯一確認一個代理。比如你可以分別代理get(int)方法在參數分別爲0和1時的不同行爲

@Spy

與@Mock不同的是,mock對象是基於真實的實例創建,對mock對象的方法調用同時也會調用真實對象的方法;

@Test
public void whenSpyingOnList_thenCorrect() {
    List<String> list = new ArrayList<String>();
    List<String> spyList = Mockito.spy(list);
 
    spyList.add("one");
    spyList.add("two");
 
    Mockito.verify(spyList).add("one");
    Mockito.verify(spyList).add("two");
 
    assertEquals(2, spyList.size());
}

可以像使用mock對象時的語法一樣,配置或者說重新改寫spy對象的方法,下面的例子使用了doReturn()來重寫了size()方法:

@Test
public void whenStubASpy_thenStubbed() {
    List<String> list = new ArrayList<String>();
    List<String> spyList = Mockito.spy(list);
 
    assertEquals(0, spyList.size());
 
    Mockito.doReturn(100).when(spyList).size();
    assertEquals(100, spyList.size());
}

注:

  • 在監控對象上使用when(Object)來進行打樁有時候是不可能或者不切實際的。

    在使用spy的時候,推薦用 doReturn|Answer|Throw() 等一系列的方法來打樁。

    List list = new LinkedList();
    List spy = spy(list);
    
    // 不可能:
    // 因爲當調用spy.get(0)時會調用真實對象的get(0)函數,
    // 此時會發生IndexOutOfBoundsException,因爲真實list對象是空的。
    when(spy.get(0)).thenReturn("foo");
    
    // 你需要使用 doReturn來進行打樁。
    doReturn("foo").when(spy).get(0);
    
  • 另外注意, Mockito並不會爲真實對象代理函數調用,實際上它會複製真實對象

    所以,如果你保留了真實對象並與其交互,不要期望從監控對象得到正確的結果。
    當你在監控對象上調用一個沒有被打樁的函數時,並不會調用真實對象的對應函數,因此你不會在真實對象上看到任何結果。

  • Mock vs. Spy

    當Mockito創造一個mock對象時,它是從類的類型中創建,而不是從類的實例中創建。也就是說mock對象是一個簡單的只帶有骨架的空殼的對象實例,同時我們可以跟蹤它的所有交互方法。不同的是,spy對象是對一個對象實例進行的封裝,它仍然能夠像正常對象實例一樣有這正常的行爲,唯一不同的是這些行爲交互是可以跟蹤和配置的。

    @Test
    public void whenCreateMock_thenCreated() {
        List mockedList = Mockito.mock(ArrayList.class);
     
        mockedList.add("one");
        Mockito.verify(mockedList).add("one");
     
        assertEquals(0, mockedList.size());
    }
    
    @Test
    public void whenCreateSpy_thenCreate() {
        List spyList = Mockito.spy(new ArrayList());
     
        spyList.add("one");
        Mockito.verify(spyList).add("one");
     
        assertEquals(1, spyList.size());
    }
    

用法指南

  • 基本用法

    Mockito.when(list.size()).thenReturn(1);
    

    上述代碼表示,當對list對象調用size()方法時,會返回1.這樣我們就可以自定義mock對象的行爲了。

  • 迭代風格的返回值設定

    // 第一種方式 
    when(i.next()).thenReturn("Hello").thenReturn("World");
    
    // 第二種方式
    when(i.next()).thenReturn("Hello", "World");
    
    // 第三種方式 
    when(i.next()).thenReturn("Hello");
    when(i.next()).thenReturn("World");
    
  • 沒有返回值的打樁

    Mockito.doNothing().when(obj).notify();
    // 或直接
    when(obj).notify();
    
  • 對被測試的方法強行拋出異常

    when(i.next()).thenThrow(new RuntimeException());
    
    // void 方法的
    doThrow(new RuntimeException()).when(i).remove(); 
    
  • 模擬傳入的參數 (Mockito.anyInt())

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

    anyString() 匹配任何 String 參數,
    anyInt() 匹配任何 int 參數,
    anySet() 匹配任何 Set,
    any() 則意味着參數爲任意值
    再進一步,自定義類型也可以,如 any(User.class)
    自定義參數匹配器 可以實現 ArgumentMatcher<?> 接口,
    然後 when(mock.addAll(argThat(new YourImplClass()))).thenReturn(true)

  • 獲取返回的結果: 實現 Answer<?>接口

    final Map<String, Object> hash = new HashMap<String, Object>();
    Answer<String> answer = new Answer<String>() {  
       public String answer(InvocationOnMock invocation) {  
           Object[] args = invocation.getArguments();  
           return hash.get(args[0].toString()).toString();  
       } 
    };
    when(request.getAttribute("errMsg")).thenAnswer(answer); 
    

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

  • getArguments() 調用後會以 Object 數組的方式返回 mock 方法調用的參數。

  • getMethod() 返回 java.lang.reflect.Method 對象

  • getMock() 返回 mock 對象

  • callRealMethod() 真實方法調用,如果 mock 的是接口它將會拋出異常

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

    Answer<String> answer = new Answer<Object>() {  
       public String answer(InvocationOnMock invocation) {  
           Object[] args = invocation.getArguments();  
           System.out.println(args[1]);
           hash.put(args[0].toString(), args[1]);
           return "called with arguments: " + args; 
       } 
    };
    doAnswer(answer).when(request).setAttribute(anyString(), anyString());
    
  • 驗證方法

    前面提到的 when(……).thenReturn(……) 屬於狀態測試,
    某些時候,測試不關心返回結果,而是側重方法有否被正確的參數調用過,這時候就應該使用 驗證方法了。
    從概念上講,就是和狀態測試所不同的“行爲測試”了。
    一旦使用 mock() 對模擬對象打樁,意味着 Mockito 會記錄着這個模擬對象調用了什麼方法,還有調用了多少次。
    最後由用戶決定是否需要進行驗證,即 verify() 方法。

    mockedList.add("one");
    mockedList.add("two");
    
    // 如果times不傳入,則默認是1, 表示驗證 mockedList 是否被調用了 add("one")方法1次。
    verify(mockedList,times(1)).add("one");
    
    Map mock = Mockito.mock( Map.class );
    when(mock.get("city")).thenReturn("廣州");
    // 關注參數有否傳入
    verify(mock).get(Matchers.eq("city")); 
     
    
    // verify 也可以使用模擬參數,但是若方法中的某一個參數使用了matcher,則所有的參數都必須使用 matcher。
    // 例如 下面的語句會拋出異常,
    verify(mock).someMethod(anyInt(), anyString(), "third argument"); 
     
    // 關注調用的次數
    verify(mock, times(2)).get(Matchers.eq("city")); 
    //還有 never()==times(0)、atLeast(N)、atLeastOnce()===atLeast(1)、atMost(N)
    
    //驗證mock對象是否存在沒有驗證過的調用方法
    verifyNoMoreInteractions(mock) ; 
    
    //驗證mock對象是否沒有進行任何交互
    verifyZeroInteractions(mock); 
    
    //超時驗證 單位毫秒
    verify(mock, timeout(100)).someMethod(); 
    
    //在給定的時間內完成執行次數 
    verify(mock, timeout(100).times(2)).someMethod(); 
    
    //給定的時間內至少執行兩次
    verify(mock, timeout(100).atLeast(2)).someMethod(); 
     
    //還有  驗證方法調用的順序 inOrder,不再詳述。
    //還有 驗證參數交互時所傳入方法的參數,使用 ArgumentCaptor argument = ArgumentCaptor.forClass(String.class);  不再詳述
    
  • 參數捕捉

    @Test
    public void testCapturingArguments() throws Exception {
        List mockedList = mock(List.class);
        ArgumentCaptor<String> argument = ArgumentCaptor.forClass(String.class);
        mockedList.add("John");
        //驗證後再捕捉參數
        verify(mockedList).add(argument.capture());
        //驗證參數
        assertEquals("John", argument.getValue());
    }
    

    注:可用 @Captor 創建ArgumentCaptor

  • 部分Mock

    //用spy()方法創建部分mock
    List list = spy(new LinkedList());
    
    //用thenCallRealMethod方法
    when(mock.someMethod()).thenCallRealMethod();
    

原理淺析

  • 標記爲InjectMocks的對象注入的mock對象來自哪裏?

    標記爲InjectMocks的字段注入的依賴mock爲同一testInstance(一個測試類爲一個testInstance)下的其他mock字段。

    通過調用方法 MockitoAnnotations.initMocks(Object testInstance) 實現Mock注入:先分類無依賴mock和有依賴mock;然後先創建無依賴的mock實例,再將創建好的無依賴mock對象集作爲參數創建依有依賴mock實例。
    在這裏插入圖片描述

進階——PowerMock

Mockito可以實現常規的一些測試需求,如果有很難甚至無法測試的測試問題,可以考慮PowerMock。PowerMock可以模擬靜態方法,刪除靜態初始化程序,允許模擬而不依賴於注入,等等。PowerMock通過在執行測試時在運行時修改字節碼來完成這些技巧。PowerMock還包含一些實用程序,可以更輕鬆地訪問對象的內部狀態。

參考

mockito wiki
Mockito 源碼解析
Mockito Spy 用法
Mockito測試
Mockito使用指南

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章