Mockito與PowerMock的使用基礎教程

magicTan發表於2018-11-11

一、Mockito與PowerMock簡述

    Mockito與PowerMock都是Java流行的一種Mock框架,使用Mock技術能讓我們隔離外部依賴以便對我們自己的業務邏輯程式碼進行單元測試,在編寫單元測試時,不需要再進行繁瑣的初始化工作,在需要呼叫某一個介面時,直接模擬一個假方法,並任意指定方法的返回值。     Mockito的工作原理是通過建立依賴物件的proxy,所有的呼叫先經過proxy物件,proxy物件攔截了所有的請求再根據預設的返回值進行處理。PowerMock則在Mockito原有的基礎上做了擴充套件,通過修改類位元組碼並使用自定義ClassLoader載入執行的方式來實現mock靜態方法、final方法、private方法、系統類的功能。   從兩者的專案結構中就可以看出,PowerMock直接依賴於Mockito,所以如果專案中已經匯入了PowerMock包就不需要再單獨匯入Mockito包,如果兩者同時匯入還要小心PowerMock和Mockito不同版本之間的相容問題:

  1. Mockito包依賴:

Mockito maven依賴

<dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <version>2.23.0</version>
        <scope>test</scope>
</dependency>
複製程式碼
  1. PowerMock包依賴:

PowerMock maven依賴

<dependency>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-module-junit4</artifactId>
        <version>2.0.0-RC.3</version>
        <scope>test</scope>
</dependency>
<dependency>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-api-mockito2</artifactId>
        <version>2.0.0-RC.3</version>
        <scope>test</scope>
</dependency>
複製程式碼

二、Mockito的使用

    Mockito一般通過建立mock或spy物件,並制定具體返回規則來實現模擬的功能,在呼叫完成後還可以進行方法呼叫驗證以檢驗程式邏輯是否正確。mock和spy物件的區別是mock物件對於未指定處理規則的呼叫會按方法返回值型別返回該型別的預設值(如int、long則返回0,boolean則返回false,物件則返回null,void則什麼都不做),而spy物件在未指定處理規則時則會直接呼叫真實方法。

以下3個類是我們的專案中需要用到的一些業務類:

//實體類
public class Node {
    private int num;
    private String name;

    //以下忽略所有構造方法和get、set方法
}

//本地負責實現具體業務的業務類
public class LocalServiceImpl implements ILocalService {

    //外部依賴
    @Autowired
    private IRemoteService remoteService;

    //具體業務處理方法
    @Override
    public Node getRemoteNode(int num) {
        return remoteService.getRemoteNode(num);
    }
    //以下忽略其他業務呼叫方法,在後面例子中補充
}

//外部依賴業務類,由其他人實現,可能我們的業務類寫好了別人還沒寫好
public class RemoteServiceImpl implements IRemoteService {
    //外部類提供的一些業務方法
    @Override
    public Node getRemoteNode(int num) {
        return new Node(num, "Node from remote service");
    }
    //其他業務方法在後面例子中補充
}
複製程式碼

下面是Mockito具體使用的一些示例:

  1. mock外部依賴物件,並注入到我們的業務類中,以便在單元測試中進行模擬呼叫:
@RunWith(MockitoJUnitRunner.class) //讓測試執行於Mockito環境
public class LocalServiceImplMockTest {

    @InjectMocks    //此註解表示這個物件需要被注入mock物件
    private LocalServiceImpl localService;
    @Mock   //此註解會自動建立1個mock物件並注入到@InjectMocks物件中
    private RemoteServiceImpl remoteService;

    //如果不使用上述註解,可以使用@Before方法來手動進行mock物件的建立和注入,但會幾行很多程式碼
    /*
    private LocalServiceImpl localService;
    private RemoteServiceImpl remoteService;

    @Before
    public void setUp() throws Exception {
        localService = new LocalServiceImpl();
        remoteService = Mockito.mock(RemoteServiceImpl.class);  //建立Mock物件
        Whitebox.setInternalState(localService, "remoteService", remoteService); //注入依賴物件
    }
    */

    @Test
    public void testMock() {
        Node target = new Node(1, "target");    //建立一個Node物件作為返回值
        Mockito.when(remoteService.getRemoteNode(1)).thenReturn(target); //指定當remoteService.getRemoteNode(int)方法傳入引數為1時返回target物件
        Node result = localService.getRemoteNode(1);    //呼叫我們的業務方法,業務方法內部呼叫依賴物件方法
        assertEquals(target, result);   //可以斷言我們得到的返回值其實就是target物件
        assertEquals(1, result.getNum());   //具體屬性和我們指定的返回值相同
        assertEquals("target", result.getName());   //具體屬性和我們指定的返回值相同

        Node result2 = localService.getRemoteNode(2);   //未指定引數為2時對應的返回規則
        assertNull(result2);    //未指定時返回為null
    }
}
複製程式碼
  1. spy外部依賴物件,並注入到我們的業務類中:
@RunWith(MockitoJUnitRunner.class)
public class LocalServiceImplSpyTest {

    @InjectMocks
    private LocalServiceImpl localService;
    @Spy    //注意這裡使用的是@Spy註解
    private RemoteServiceImpl remoteService;
    //注意如果自己建立spy物件的話要這麼寫:
    /*
        remoteService = new RemoteServiceImpl();    //先建立一個具體例項
        remoteService = Mockito.spy(remoteService);   //再呼叫Mockito.spy(T)方法建立spy物件
    */

    @Test
    public void testSpy() {
        Node target = new Node(1, "target");    //建立一個Node物件作為返回值
        Mockito.when(remoteService.getRemoteNode(1)).thenReturn(target); //指定當remoteService.getRemoteNode(int)方法傳入引數為1時返回target物件
        Node result = localService.getRemoteNode(1);    //呼叫我們的業務方法,業務方法內部呼叫依賴物件方法
        assertEquals(target, result);   //可以斷言我們得到的返回值其實就是target物件
        assertEquals(1, result.getNum());   //具體屬性和我們指定的返回值相同
        assertEquals("target", result.getName());   //具體屬性和我們指定的返回值相同

        Node result2 = localService.getRemoteNode(2);   //未指定引數為2時的呼叫規則,所以會直接呼叫真實物件,返回remote建立的節點
        assertEquals(2, result2.getNum());
        assertEquals("Node from remote service", result2.getName());    //remoteService建立Node物件時設定name屬性為"Node from remote service"
    }
}
複製程式碼
  1. 使用ArgumentMatchers的any系列方法指定多種返回值,有any()、anyInt()、anyString()、anyByte()、anyLong()等等,可以看下ArgumentMatchers類原始碼中定義的所有方法:
    @Test
    public void testAny() {
        Node target = new Node(1, "target");
        when(remoteService.getRemoteNode(anyInt())).thenReturn(target); //靜態匯入Mockito.when和ArgumentMatchers.anyInt後可以簡化程式碼提升可讀性

        Node result = localService.getRemoteNode(20); //上面指定了呼叫remoteService.getRemoteNode(int)時,不管傳入什麼引數都會返回target物件
        assertEquals(target, result);   //可以斷言我們得到的返回值其實就是target物件
        assertEquals(1, result.getNum());   //具體屬性和我們指定的返回值相同
        assertEquals("target", result.getName());   //具體屬性和我們指定的返回值相同
    }
複製程式碼
  1. 指定mock物件多次呼叫的返回值:
    /**
     * 指定mock多次呼叫返回值
     */
    @Test
    public void testMultipleReturn() {
        Node target1 = new Node(1, "target");
        Node target2 = new Node(1, "target");
        Node target3 = new Node(1, "target");
        when(remoteService.getRemoteNode(anyInt())).thenReturn(target1).thenReturn(target2).thenReturn(target3);
        //第一次呼叫返回target1、第二次返回target2、第三次返回target3

        Node result1 = localService.getRemoteNode(1); //第1次呼叫
        assertEquals(target1, result1);
        Node result2 = localService.getRemoteNode(2); //第2次呼叫
        assertEquals(target2, result2);
        Node result3 = localService.getRemoteNode(3); //第3次呼叫
        assertEquals(target3, result3);
    }
複製程式碼
  1. 指定mock物件丟擲異常(注意如果方法中未宣告會丟擲異常,只能指定丟擲執行時異常,如果仍指定為丟擲受檢查異常,執行時會報錯誤org.mockito.exceptions.base.MockitoException: Checked exception is invalid for this method!):
    //RemoteServiceImpl方法:
    @Override
    public Node getRemoteNode(String name) throws MockException {
        if (StringUtils.isEmpty(name)) {
            throw new MockException("name不能為空", name);
        }
        return new Node(name);
    }

    //LocalServiceImpl方法
    @Override
    public Node getRemoteNode(String name) throws MockException {
        try {
            return remoteService.getRemoteNode(name);
        } catch (IllegalArgumentException e) {
            throw e;
        }
    }

    /**
     * 指定mock物件已宣告異常丟擲的方法丟擲受檢查異常
     */
    @Test
    public void testExceptionDeclare() {
        try {
            Node target = new Node(1, "target");
            when(remoteService.getRemoteNode("name")).thenReturn(target).thenThrow(new MockException(
                    "message", "exception")); //第一次呼叫正常返回,第二次則丟擲一個Exception

            Node result1 = localService.getRemoteNode("name");
            assertEquals(target, result1); //第一次呼叫正常返回

            Node result2 = localService.getRemoteNode("name"); //第二次呼叫不會正常返回,會丟擲異常
            assertEquals(target, result2);
        } catch (MockException e) {
            assertEquals("exception", e.getName()); //驗證是否返回指定異常內容
            assertEquals("message", e.getMessage()); //驗證是否返回指定異常內容
        }
    }

    /**
     * 指定mock物件為宣告異常丟擲的方法丟擲執行時異常
     */
    @Test
    public void testRuntimeException() {
        Node target = new Node(1, "target");
        when(remoteService.getRemoteNode(1)).thenThrow(new RuntimeException("exception")); //指定呼叫時丟擲一個執行時異常

        try {
            Node result = localService.getRemoteNode(1);
            assertEquals(target, result);
        } catch (RuntimeException e) {
            assertEquals("exception", e.getMessage());
        }
    }

    /**
     * 指定mock物件未宣告異常丟擲的方法丟擲受檢查異常,以下方法執行會報錯
     */
    @Test
    public void testNotDefineCheckedException() {
        Node target = new Node(1, "target");
        when(remoteService.getRemoteNode(1)).thenThrow(new IOException("io exception"));

        try {
            Node result = localService.getRemoteNode(1);
            assertEquals(target, result);
        } catch (Exception e) {
            assertEquals("io exception", e.getMessage());
        }
    }
複製程式碼
  1. mock void方法拋異常、什麼都不做:
    //RemoteServiceImpl方法:
    @Override
    public void doSometing() {
        System.out.println("remote service do something!");
    }

    //LocalServiceImpl方法
    @Override
    public void remoteDoSomething() {
        remoteService.doSometing();
    }

    //注意void方法沒有返回值,所以mock規則寫法順序不一樣
    doNothing().when(remoteService).doSometing();
    doThrow(new RuntimeException("exception")).when(remoteService).doSometing();
複製程式碼
  1. 校驗mock物件的呼叫情況(除Mockito中的never()、times(int)方法外,還有atLeast(int)、atLeastOne()、atMost(int)等方法):
   /**
     * 校驗mock物件和方法的呼叫情況
     *
     */
    public void testVerify() {
        Node target = new Node(1, "target");
        when(remoteService.getRemoteNode(anyInt())).thenReturn(target);

        verify(remoteService, never()).getRemoteNode(1); //mock方法未呼叫過

        localService.getRemoteNode(1);
        Mockito.verify(remoteService, times(1)).getRemoteNode(anyInt()); //目前mock方法呼叫過1次

        localService.getRemoteNode(2);
        verify(remoteService, times(2)).getRemoteNode(anyInt()); //目前mock方法呼叫過2次
        verify(remoteService, times(1)).getRemoteNode(2); //目前mock方法引數為2只呼叫過1次
    }
複製程式碼
  1. 利用ArgumentCaptor捕獲方法引數進行mock方法引數校驗
    /**
     * 利用ArgumentCaptor捕獲方法引數進行mock方法引數校驗
     */
    @Test
    public void testCaptor() throws Exception {
        Node target = new Node(1, "target");
        when(remoteService.getRemoteNode(anyString())).thenReturn(target);

        localService.getRemoteNode("name1");
        localService.getRemoteNode("name2");
        verify(remoteService, atLeastOnce()).getRemoteNode(localCaptor.capture()); //設定captor

        assertEquals("name2", localCaptor.getValue()); //獲取最後一次呼叫的引數
        List<String> list = localCaptor.getAllValues(); //按順序獲取所有傳入的引數
        assertEquals("name1", list.get(0));
        assertEquals("name2", list.get(1));
    }
複製程式碼
  1. mock物件呼叫真實方法:
    /**
     * mock物件呼叫真實方法
     */
    @Test
    public void testCallRealMethod() {
        when(remoteService.getRemoteNode(anyInt())).thenCallRealMethod(); //設定呼叫真實方法
        Node result = localService.getRemoteNode(1);

        assertEquals(1, result.getNum());
        assertEquals("Node from remote service", result.getName());
    }
複製程式碼
  1. 重置mock物件:
    //重置mock,清除所有的呼叫記錄和返回規則
    Mockito.reset(remoteService);
複製程式碼
  1. 校驗mock物件0呼叫和未被驗證的呼叫
    /**
     * 校驗mock物件0呼叫和未被驗證的呼叫
     */
    @Test(expected = NoInteractionsWanted.class)
    public void testInteraction() {
        verifyZeroInteractions(remoteService); //目前還未被呼叫過,執行不報錯

        Node target = new Node(1, "target");
        when(remoteService.getRemoteNode(anyInt())).thenReturn(target);

        localService.getRemoteNode(1);
        localService.getRemoteNode(2);
        verify(remoteService, times(2)).getRemoteNode(anyInt());
        // 引數1和2的兩次呼叫都會被上面的anyInt()校驗到,所以沒有未被校驗的呼叫了
        verifyNoMoreInteractions(remoteService);

        reset(remoteService);
        localService.getRemoteNode(1);
        localService.getRemoteNode(2);
        verify(remoteService, times(1)).getRemoteNode(1);
        // 引數2的呼叫不會被上面的校驗到,所以執行會拋異常
        verifyNoMoreInteractions(remoteService);
    }
複製程式碼

三、PowerMock的使用

PowerMock的使用與Mockito有一些不同,首先是測試類上的@RunWith註解需要修改為:

@RunWith(PowerMockRunner.class)
複製程式碼

第二是需要使用到@PrepareForTest註解(PrepareFotTest註解會修改傳入引數類的位元組碼,通過修改位元組碼達到模擬final、static、私有方法、系統類等的功能),此註解可寫在類上也可寫在方法上:

@PrepareForTest(RemoteServiceImpl.class)
複製程式碼
  1. mock new關鍵字
    //LocalServiceImpl
    @Override
    public Node getLocalNode(int num, String name) {
        return new Node(num, name);
    }

    /**
     * mock new關鍵字
     */
    @Test
    @PrepareForTest(LocalServiceImpl.class) //PrepareForTest修改local類的位元組碼以覆蓋new的功能
    public void testNew() throws Exception {
        Node target = new Node(1, "target");
        //當傳入任意int且name屬性為"name"時,new物件返回為target
        //當引數條件使用了any系列方法時,剩餘的引數都得使用相應的模糊匹配規則,如eq("name")代表引數等於"name"
        //剩餘還有isNull(), isNotNull(), isA()等方法
        PowerMockito.whenNew(Node.class).withArguments(anyInt(), eq("name")).thenReturn(target);
        Node result = localService.getLocalNode(2, "name");
        assertEquals(target, result); //返回值為target
        assertEquals(1, result.getNum());
        assertEquals("target", result.getName());

        //未指定name為"test"的返回值,預設返回null
        Node result2 = localService.getLocalNode(1, "test");
        assertNull(result2);
    }
複製程式碼
  1. mock final方法
    //RemoteServiceImpl
    @Override
    public final Node getFinalNode() {
        return new Node(1, "final node");
    }

    /**
     * mock final方法
     */
    @Test
    @PrepareForTest(RemoteServiceImpl.class) //final方法在RemoteServiceImpl類中
    public void testFinal() {
        Node target = new Node(2, "mock");
        PowerMockito.when(remoteService.getFinalNode()).thenReturn(target); //指定返回值

        Node result = remoteService.getFinalNode(); //直接呼叫final方法,返回mock後的值
        assertEquals(target, result); //驗證返回值
        assertEquals(2, result.getNum());
        assertEquals("mock", result.getName());
    }
複製程式碼
  1. mock static方法
    //Node
    public static Node getStaticNode() {
        return new Node(1, "static node");
    }

    /**
     * mock static方法
     */
    @Test
    @PrepareForTest(Node.class) //static方法定義在Node類中
    public void testStatic() {
        Node target = new Node(2, "mock");
        PowerMockito.mockStatic(Node.class); //mock static方法前需要加這一句
        PowerMockito.when(Node.getStaticNode()).thenReturn(target); //指定返回值

        Node result = Node.getStaticNode(); //直接呼叫static方法,返回mock後的值
        assertEquals(target, result); //驗證返回值
        assertEquals(2, result.getNum());
        assertEquals("mock", result.getName());
    }
複製程式碼
  1. mock private方法
    //RemoteServiceImpl
    @Override
    public Node getPrivateNode() {
        return privateMethod();
    }

    //RemoteServiceImpl
    private Node privateMethod() {
        return new Node(1, "private node");
    }

    /**
     * mock 私有方法
     */
    @Test
    @PrepareForTest(RemoteServiceImpl.class) //private方法定義在RemoteServiceImpl類中
    public void testPrivate() throws Exception {
        Node target = new Node(2, "mock");
        //按照真實程式碼呼叫privateMethod方法
        PowerMockito.when(remoteService.getPrivateNode()).thenCallRealMethod();
        //私有方法無法訪問,類似反射傳遞方法名和引數,此處無引數故未傳
        PowerMockito.when(remoteService, "privateMethod").thenReturn(target);

        Node result = remoteService.getPrivateNode();
        assertEquals(target, result); //驗證返回值
        assertEquals(2, result.getNum());
        assertEquals("mock", result.getName());
    }
複製程式碼
  1. mock 系統類方法
    //RemoteServiceImpl
    @Override
    public Node getSystemPropertyNode() {
        return new Node(System.getProperty("abc"));
    }

    /**
     * mock 系統類方法
     */
    @Test
    @PrepareForTest(RemoteServiceImpl.class) //類似new關鍵字,系統類方法的呼叫在類RemoteServiceImpl中,所以這裡填的是RemoteServiceImpl
    public void testSystem() {
        PowerMockito.mockStatic(System.class); //呼叫的是系統類的靜態方法,所以要加這一句
        PowerMockito.when(System.getProperty("abc")).thenReturn("mock"); //設定System.getProperty("abc")返回"mock"
        PowerMockito.when(remoteService.getSystemPropertyNode()).thenCallRealMethod(); //設定mock物件呼叫實際方法

        Node result = remoteService.getSystemPropertyNode(); //按程式碼會返回一個name屬性為"mock"的物件
        assertEquals(0, result.getNum()); //int預設值為0
        assertEquals("mock", result.getName()); //remoteService物件中呼叫System.getProperty("abc")返回的是上面設定的"mock"
    }
複製程式碼

專案程式碼已上傳至GitHub:MockDemo

相關文章