Bootstrap

Mockito单元测试简述

参考文章

《Mockito英文版javadoc》
《Mockito中文文档(部分)》
《Mockito使用教程》
《参数捕获器使用》
《利用ArgumentCaptor(参数捕获器)捕获方法参数进行验证》
《改变mock返回值》
《五分钟了解Mockito》
《使用Mockito进行单元测试》
《JUnit + Mockito 单元测试》
《Mockito中@Mock与@InjectMock》
《mockito中两种部分mock的实现,spy、callRealMethod》
《Mockito 中被 Mocked 的对象属性及方法的默认值》
《单元测试工具之Mockito》
《引入Mockito测试用@Spy和@Mock》
《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
    }
;