Mockito單元測試簡述

一、準備工作

引入maven依賴

    <!--mockito依賴-->
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <version>2.7.19</version>
        <scope>test</scope>
    </dependency>
    <!-- junit依賴 -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>

二、概念

1)Mockito:簡單輕量級的做mocking測試的框架;
2)mock對象:在調試期間用來作爲真實對象的替代品;
3)mock測試:在測試過程中,對那些不容易構建的對象用一個虛擬對象來代替測試的方法就叫mock測試;
4)stub:打樁,就是爲mock對象的方法指定返回值(可拋出異常);
5)verify:行爲驗證,驗證指定方法調用情況(是否被調用,調用次數等);

三、具體操作

/**
     * 基礎部分
     */
    @Test
    public void test0() {
        //1、創建mock對象(模擬依賴的對象)
        final List mock = Mockito.mock(List.class);

        //2、使用mock對象(mock對象會對接口或類的方法給出默認實現)
        System.out.println("mock.add result => " + mock.add("first"));  //false
        System.out.println("mock.size result => " + mock.size());       //0

        //3、打樁操作(狀態測試:設置該對象指定方法被調用時的返回值)
        Mockito.when(mock.get(0)).thenReturn("second");
        Mockito.doReturn(66).when(mock).size();

        //3、使用mock對象的stub(測試打樁結果)
        System.out.println("mock.get result => " + mock.get(0));    //second
        System.out.println("mock.size result => " + mock.size());   //66

        //4、驗證交互 verification(行爲測試:驗證方法調用情況)
        Mockito.verify(mock).get(Mockito.anyInt());
        Mockito.verify(mock, Mockito.times(2)).size();

        //5、驗證返回的結果(這是JUnit的功能)
        assertEquals("second", mock.get(0));
        assertEquals(66, mock.size());
    }

    /**
     * 驗證某些行爲:
     * 一旦mock對象被創建了,mock對象會記住所有的交互。
     * 然後你就可能選擇性的驗證你感興趣的交互。驗證不通過則拋出異常
     */
    @Test
    public void test1() {
        final List mockList = Mockito.mock(List.class);
        mockList.add("mock1");
        mockList.get(0);
        mockList.size();
        mockList.clear();
        // 驗證方法被使用(默認1次)
        Mockito.verify(mockList).add("mock1");
        // 驗證方法被使用1次
        Mockito.verify(mockList, Mockito.times(1)).get(0);
        // 驗證方法至少被使用1次
        Mockito.verify(mockList, Mockito.atLeast(1)).size();
        // 驗證方法沒有被使用
        Mockito.verify(mockList, Mockito.never()).contains("mock2");
        // 驗證方法至多被使用5次
        Mockito.verify(mockList, Mockito.atMost(5)).clear();
        // 指定方法調用超時時間
        Mockito.verify(mockList, timeout(100)).get(0);
        // 指定時間內需要完成的次數
        Mockito.verify(mockList, timeout(200).atLeastOnce()).size();
    }

    /**
     * 如何做一些測試樁stub:
     * 1、默認情況下,所有的函數都有返回值。mock函數默認返回的是null,
     * 一個空的集合或者一個被對象類型包裝的內置類型,例如0、false對應的對象類型爲Integer、Boolean;
     * 2、一旦測試樁函數被調用,該函數將會一致返回固定的值;
     * 3、對於 static 和 final 方法, Mockito 無法對其 when(…).thenReturn(…) 操作。
     */
    @Test
    public void test2() {
        //靜態導入,減少代碼量:import static org.mockito.Mockito.*;
        final ArrayList mockList = mock(ArrayList.class);

        // 設置方法調用返回值
        when(mockList.add("test2")).thenReturn(true);
        doReturn(true).when(mockList).add("test2");
        System.out.println(mockList.add("test2"));  //true

        // 設置方法調用拋出異常
        when(mockList.get(0)).thenThrow(new RuntimeException());
        doThrow(new RuntimeException()).when(mockList).get(0);
        System.out.println(mockList.get(0));    //throw RuntimeException

        // 無返回方法打樁
        doNothing().when(mockList).clear();

        // 爲回調做測試樁(對方法返回進行攔截處理)
        final Answer<String> answer = new Answer<String>() {
            @Override
            public String answer(InvocationOnMock invocationOnMock) throws Throwable {
                final List mock = (List) invocationOnMock.getMock();
                return "mock.size result => " + mock.size();
            }
        };
        when(mockList.get(1)).thenAnswer(answer);
        doAnswer(answer).when(mockList).get(1);
        System.out.println(mockList.get(1));    //mock.size result => 0

        // 對同一方法多次打樁,以最後一次爲準
        when(mockList.get(2)).thenReturn("test2_1");
        when(mockList.get(2)).thenReturn("test2_2");
        System.out.println(mockList.get(2));    //test2_2
        System.out.println(mockList.get(2));    //test2_2

        // 設置多次調用同類型結果
        when(mockList.get(3)).thenReturn("test2_1", "test2_2");
        when(mockList.get(3)).thenReturn("test2_1").thenReturn("test2_2");
        System.out.println(mockList.get(3));    //test2_1
        System.out.println(mockList.get(3));    //test2_2

        // 爲連續調用做測試樁(爲同一個函數調用的不同的返回值或異常做測試樁)
        when(mockList.get(4)).thenReturn("test2").thenThrow(new RuntimeException());
        doReturn("test2").doThrow(new RuntimeException()).when(mockList).get(4);
        System.out.println(mockList.get(4));    //test2
        System.out.println(mockList.get(4));    //throw RuntimeException

        // 無打樁方法,返回默認值
        System.out.println(mockList.get(99));    //null
    }

    /**
     * 參數匹配器(matchers):
     * 1、參數匹配器使驗證和測試樁變得更靈活。
     * 2、爲了合理的使用複雜的參數匹配,使用equals()與anyX() 的匹配器會使得測試代碼更簡潔、簡單。
     * 有時,會迫使你重構代碼以使用equals()匹配或者實現equals()函數來幫助你進行測試。
     * 3、如果你使用參數匹配器,所有參數都必須由匹配器提供。
     * 4、支持自定義參數匹配器。
     */
    @Test
    public void test3() {
        final Map mockMap = mock(Map.class);

        // 正常打樁測試
        when(mockMap.get("key")).thenReturn("value1");
        System.out.println(mockMap.get("key"));     //value1

        // 爲靈活起見,可使用參數匹配器
        when(mockMap.get(anyString())).thenReturn("value2");
        System.out.println(mockMap.get(anyString()));   //value2
        System.out.println(mockMap.get("test_key"));    //value2
        System.out.println(mockMap.get(0)); //null

        // 多個入參時,要麼都使用參數匹配器,要麼都不使用,否則會異常
        when(mockMap.put(anyString(), anyInt())).thenReturn("value3");
        System.out.println(mockMap.put("key3", 3));     //value3
        System.out.println(mockMap.put(anyString(), anyInt()));     //value3
        System.out.println(mockMap.put("key3", anyInt()));    //異常

        // 行爲驗證時,也支持使用參數匹配器
        verify(mockMap, atLeastOnce()).get(anyString());
        verify(mockMap).put(anyString(), eq(3));

        // 自定義參數匹配器
        final ArgumentMatcher<ArgumentTestRequest> myArgumentMatcher = new ArgumentMatcher<ArgumentTestRequest>() {
            @Override
            public boolean matches(ArgumentTestRequest request) {
                return "name".equals(request.getName()) || "value".equals(request.getValue());
            }
        };
        // 自定義參數匹配器使用
        final ArgumentTestService mock = mock(ArgumentTestService.class);
        when(mock.argumentTestMethod(argThat(myArgumentMatcher))).thenReturn("success");
        doReturn("success").when(mock).argumentTestMethod(argThat(myArgumentMatcher));
        System.out.println(mock.argumentTestMethod(new ArgumentTestRequest("name", "value")));  // success
        System.out.println(mock.argumentTestMethod(new ArgumentTestRequest()));     //null
    }

    /**
     * 執行順序驗證:
     * 1、驗證執行順序是非常靈活的-你不需要一個一個的驗證所有交互,只需要驗證你感興趣的對象即可;
     * 2、你可以僅通過那些需要驗證順序的mock對象來創建InOrder對象;
     */
    @Test
    public void test4() {
        // 驗證同一個對象多個方法的執行順序
        final List mockList = mock(List.class);
        mockList.add("first");
        mockList.add("second");
        final InOrder inOrder = inOrder(mockList);
        inOrder.verify(mockList).add("first");
        inOrder.verify(mockList).add("second");

        // 驗證多個對象多個方法的執行順序
        final List mockList1 = mock(List.class);
        final List mockList2 = mock(List.class);
        mockList1.get(0);
        mockList1.get(1);
        mockList2.get(0);
        mockList1.get(2);
        mockList2.get(1);
        final InOrder inOrder1 = inOrder(mockList1, mockList2);
        inOrder1.verify(mockList1).get(0);
        inOrder1.verify(mockList1).get(2);
        inOrder1.verify(mockList2).get(1);
    }

    /**
     * 確保交互(interaction)操作不會執行在mock對象上
     * 注意:一些用戶可能會在頻繁地使用verifyNoMoreInteractions(),甚至在每個測試函數中都用。
     *      但是verifyNoMoreInteractions()並不建議在每個測試函數中都使用。
     *      verifyNoMoreInteractions()在交互測試套件中只是一個便利的驗證,它的作用是當你需要驗證是否存在冗餘調用時。
     */
    @Test
    public void test5() {
        // 驗證某個交互是否從未被執行
        final List mock = mock(List.class);
        mock.add("first");
        verify(mock, never()).add("test5");   //通過
        verify(mock, never()).add("first");  //異常

        // 驗證mock對象沒有交互過
        final List mock1 = mock(List.class);
        final List mock2 = mock(List.class);
        verifyZeroInteractions(mock1);  //通過
        verifyNoMoreInteractions(mock1, mock2); //通過
        verifyZeroInteractions(mock, mock2);  //異常

        // 注意:可能只想驗證前面的邏輯,但是加上最後一行,會導致出現異常。建議使用方法層面的驗證,如:never();
        //      在驗證是否有冗餘調用的時候,可使用此種方式。如下:
        final List mockList = mock(List.class);
        mockList.add("one");
        mockList.add("two");
        verify(mockList).add("one");    // 通過
        verify(mockList, never()).get(0);    //通過
        verifyZeroInteractions(mockList);   //異常
    }

    /**
     * 簡化mock對象創建:
     * 1、使用註解簡化mock對象創建;
     *
     * 注意!下面這句代碼需要在運行測試函數之前被調用,一般放到測試類的基類或者test runner中:
     *      MockitoAnnotations.initMocks(this);
     *      也可以使用內置的runner:MockitoJUnitRunner 或者一個rule : MockitoRule。
     */
    // 代替 mock(ArgumentTestService.class) 創建mock對象;
    @Mock
    private ArgumentTestService argumentTestService;
    // 若改註解修飾的對象有成員變量,@Mock定義的mock對象會被自動注入;
    @InjectMocks
    private MockitoAnnotationService mockitoAnnotationService = new MockitoAnnotationServiceImpl();

    @Test
    public void test6() {
        // 注意!下面這句代碼需要在運行測試函數之前被調用,一般放到測試類的基類或者test runner中;一般用於Spring注入+mockito聯合使用的時候
        //否則,可以在類上使用【@RunWith(MockitoJUnitRunner.class)】
        MockitoAnnotations.initMocks(this);
        when(argumentTestService.argumentTestMethod(new ArgumentTestRequest())).thenReturn("success");
        System.out.println(argumentTestService.argumentTestMethod(new ArgumentTestRequest()));  //success
        System.out.println(mockitoAnnotationService.mockitoAnnotationTestMethod()); //null
    }

    /**
     * 監控真實對象(部分mock):
     * 1、可以爲真實對象創建一個監控(spy)對象。當你使用這個spy對象時真實的對象也會也調用,除非它的函數被stub了。
     * 2、儘量少使用spy對象,使用時也需要小心形式,例如spy對象可以用來處理遺留代碼。
     * 3、stub語法中同樣提供了部分mock的方法,可以調用真實的方法。
     *
     * 完全mock:
     *      上文講的內容是完全mock,即創建的mock對象與真實對象無關,mock對象的方法默認都是基本的實現,返回基本類型。
     *      可基於接口、實現類創建mock對象。
     * 部分mock:
     *      所謂部分mock,即創建的mock對象時基於真實對象的,mock對象的方法都是默認使用真實對象的方法,除非stub之後,纔會以stub爲準。
     *      基於實現類創建mock對象,否則在沒有stub的情況下,調用真實方法時,會出現異常。
     *
     * 注意:
     *      Mockito並不會爲真實對象代理函數調用,實際上它會拷貝真實對象。
     *      因此如果你保留了真實對象並且與之交互,不要期望從監控對象得到正確的結果。
     *      當你在監控對象上調用一個沒有被stub的函數時並不會調用真實對象的對應函數,你不會在真實對象上看到任何效果。
     */
    @Test
    public void test7() {
        // stub部分mock(stub中使用真實調用)。注意:需要mock實現類,否則會有異常
        final StubTestService stubTestService = mock(StubTestServiceImpl.class);
        when(stubTestService.stubTestMethodA("paramA")).thenCallRealMethod();
        doCallRealMethod().when(stubTestService).stubTestMethodB();
        System.out.println(stubTestService.stubTestMethodA("paramA"));  //stubTestMethodA is called, param = paramA
        System.out.println(stubTestService.stubTestMethodB());  //stubTestMethodB is called
        System.out.println(stubTestService.stubTestMethodC());  //null

        // spy部分mock
        final LinkedList<String> linkedList = new LinkedList();
        final LinkedList spy = spy(linkedList);
        spy.add("one");
        spy.add("two");
        doReturn(100).when(spy).size();
        when(spy.get(0)).thenReturn("one_test");
        System.out.println(spy.size()); //100
        System.out.println(spy.get(0)); //one_test
        System.out.println(spy.get(1)); //two

        // spy可以類比AOP。在spy中,由於默認是調用真實方法,所以第二種寫法不等價於第一種寫法,不推薦這種寫法。
        doReturn("two_test").when(spy).get(2);
        when(spy.get(2)).thenReturn("two_test"); //異常 java.lang.IndexOutOfBoundsException: Index: 2, Size: 2
        System.out.println(spy.get(2));   //two_test

        // spy對象只是真實對象的複製,真實對象的改變不會影響spy對象
        final List<String> arrayList = new ArrayList<>();
        final List<String> spy1 = spy(arrayList);
        spy1.add(0, "one");
        System.out.println(spy1.get(0));    //one
        arrayList.add(0, "list1");
       System.out.println(arrayList.get(0));   //list1
       System.out.println(spy1.get(0));    //one

        // 若對某個方法stub之後,又想調用真實的方法,可以使用reset(spy)
        final ArrayList<String> arrayList1 = new ArrayList<>();
        final ArrayList<String> spy2 = spy(arrayList1);
        doReturn(100).when(spy2).size();
        System.out.println(spy2.size());    //100
        reset(spy2);
        System.out.println(spy2.size());    //0
    }

    /**
     * @Mock 和 @Spy的使用
     *      @Mock 等價於 Mockito.mock(Object.class);
     *      @Spy 等價於 Mockito.spy(obj);
     *
     * 區分是mock對象還是spy對象:
     *      Mockito.mockingDetails(someObject).isMock();
     *      Mockito.mockingDetails(someObject).isSpy();
     */
    @Mock
    private StubTestService stubTestService;
    @Spy
    private StubTestServiceImpl stubTestServiceImpl;
    @Spy
    private StubTestService stubTestServiceImpl1 = new StubTestServiceImpl();
    @Test
    public void test8() {
        MockitoAnnotations.initMocks(this);
        // mock對象返回默認
        System.out.println(stubTestService.stubTestMethodB());  //null
        // spy對象調用真實方法
        System.out.println(stubTestServiceImpl.stubTestMethodC());  //stubTestMethodC is called
        System.out.println(stubTestServiceImpl1.stubTestMethodA("spy"));  //stubTestMethodA is called, param = spy

        // 區分是mock對象還是spy對象
        System.out.println(mockingDetails(stubTestService).isMock());   //true
        System.out.println(mockingDetails(stubTestService).isSpy());    //false
        System.out.println(mockingDetails(stubTestServiceImpl).isSpy());    //true
    }

    /**
     * ArgumentCaptor(參數捕獲器)捕獲方法參數進行驗證:(可代替參數匹配器使用)
     *      在某些場景中,不光要對方法的返回值和調用進行驗證,同時需要驗證一系列交互後所傳入方法的參數。
     *      那麼我們可以用參數捕獲器來捕獲傳入方法的參數進行驗證,看它是否符合我們的要求。
     *  ArgumentCaptor介紹:
     *      通過ArgumentCaptor對象的forClass(Class<T> clazz)方法來構建ArgumentCaptor對象。
     *      然後便可在驗證時對方法的參數進行捕獲,最後驗證捕獲的參數值。如果方法有多個參數都要捕獲驗證,那就需要創建多個ArgumentCaptor對象處理。
     *  ArgumentCaptor的Api:
     *      argument.capture() 捕獲方法參數
     *      argument.getValue() 獲取方法參數值,如果方法進行了多次調用,它將返回最後一個參數值
     *      argument.getAllValues() 方法進行多次調用後,返回多個參數值
     *
     */
    @Test
    public void test9() {
        List mock = mock(List.class);
        List mock1 = mock(List.class);
        mock.add("John");
        mock1.add("Brian");
        mock1.add("Jim");
        // 獲取方法參數
        ArgumentCaptor argument = ArgumentCaptor.forClass(String.class);
        verify(mock).add(argument.capture());
        System.out.println(argument.getValue());    //John

        // 多次調用獲取最後一次
        ArgumentCaptor argument1 = ArgumentCaptor.forClass(String.class);
        verify(mock1, times(2)).add(argument1.capture());
        System.out.println(argument1.getValue());    //Jim

        // 獲取所有調用參數
        System.out.println(argument1.getAllValues());    //[Brian, Jim]
    }

    /**
     *  @Captor 簡化 ArgumentCaptor 的創建
     */
    @Mock
    private List<String> captorList;
    @Captor
    private ArgumentCaptor<String> argumentCaptor;
    @Test
    public void test10() {
        MockitoAnnotations.initMocks(this);
        captorList.add("cap1");
        captorList.add("cap2");
        System.out.println(captorList.size());
        verify(captorList, atLeastOnce()).add(argumentCaptor.capture());
        System.out.println(argumentCaptor.getAllValues());
    }

    /**
     * 高級特性:自定義驗證失敗信息
     */
    @Test
    public void test11() {
        final ArrayList arrayList = mock(ArrayList.class);
        arrayList.add("one");
        arrayList.add("two");

        verify(arrayList, description("size()沒有調用")).size();
        // org.mockito.exceptions.base.MockitoAssertionError: size()沒有調用

        verify(arrayList, timeout(200).times(3).description("驗證失敗")).add(anyString());
        //org.mockito.exceptions.base.MockitoAssertionError: 驗證失敗
    }

    /**
     * 高級特性:修改沒有測試樁的調用的默認返回值
     *      可以指定策略來創建mock對象的返回值。這是一個高級特性,通常來說,你不需要寫這樣的測試。
     *      然後,它對於遺留系統來說是很有用處的。當你不需要爲函數調用打樁時你可以指定一個默認的answer。
     */
    @Test
    public void test12(){
        // 創建mock對象、使用默認返回
        final ArrayList mockList = mock(ArrayList.class);
        System.out.println(mockList.get(0));    //null

        // 這個實現首先嚐試全局配置,如果沒有全局配置就會使用默認的回答,它返回0,空集合,null,等等。
        // 參考返回配置:ReturnsEmptyValues
        mock(ArrayList.class, Answers.RETURNS_DEFAULTS);

        // ReturnsSmartNulls首先嚐試返回普通值(0,空集合,空字符串,等等)然後它試圖返回SmartNull。
        // 如果最終返回對象,那麼會簡單返回null。一般用在處理遺留代碼。
        // 參考返回配置:ReturnsMoreEmptyValues
        mock(ArrayList.class, Answers.RETURNS_SMART_NULLS);

        // 未stub的方法,會調用真實方法。
        //    注1:存根部分模擬使用時(mock.getSomething ()) .thenReturn (fakeValue)語法將調用的方法。對於部分模擬推薦使用doReturn語法。
        //    注2:如果模擬是序列化反序列化,那麼這個Answer將無法理解泛型的元數據。
        mock(ArrayList.class, Answers.CALLS_REAL_METHODS);

        // 深度stub,用於嵌套對象的mock。參考:https://www.cnblogs.com/Ming8006/p/6297333.html
        mock(ArrayList.class, Answers.RETURNS_DEEP_STUBS);

        // ReturnsMocks首先嚐試返回普通值(0,空集合,空字符串,等等)然後它試圖返回mock。
        // 如果返回類型不能mocked(例如是final)然後返回null。
        mock(ArrayList.class, Answers.RETURNS_MOCKS);

        //  mock對象的方法調用後,可以返回自己(類似builder模式)
        mock(ArrayList.class, Answers.RETURNS_SELF);

        // 自定義返回
        final Answer<String> answer = new Answer<String>() {
            @Override
            public String answer(InvocationOnMock invocation) throws Throwable {
                return "test_answer";
            }
        };
        final ArrayList mockList1 = mock(ArrayList.class, answer);
        System.out.println(mockList1.get(0));   //test_answer
    }

三、參考文章

《Mockito英文版javadoc》
《Mockito中文文檔(部分)》
《Mockito使用教程》
《參數捕獲器使用》
《利用ArgumentCaptor(參數捕獲器)捕獲方法參數進行驗證》
《改變mock返回值》
《五分鐘瞭解Mockito》
《使用Mockito進行單元測試》
《JUnit + Mockito 單元測試》
《Mockito中@Mock與@InjectMock》
《mockito中兩種部分mock的實現,spy、callRealMethod》
《Mockito 中被 Mocked 的對象屬性及方法的默認值》
《單元測試工具之Mockito》
《引入Mockito測試用@Spy和@Mock》
《Mockito初探(含實例)》
《測試覆蓋率統計》
《測試覆蓋率無法統計解決》

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