Mockito初探——快速入门

Mockito是基于CGLIB代理,实现打桩。它通过拦截对象的所有操作方法,对于满足打桩条件的调用,返回预设的返回值。

主要注解

@InjectMocks

用于标记对象属性允许用mock或spy注入。尝试通过按「先构造函数注入再setter注入最后属性(字段)注入」的顺序注入依赖。

  • 构造函数注入:选取最大的构造函数,用已声明的mock作为参数注入;注:如果已经通过构造注入,将不再尝试其他策略注入(即不会再有如下两种的注入)

  • setter注入:先通过类型匹配,如果匹配的类型有多个mock实例,再通过名字匹配;

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

@Mock

用于标记一个Mock字段,被标记的对象将被创建为mock对象(对于没有打桩的方法,返回值为默认值0或null或false);

  • 允许多次代理mock对象的同一个方法,但具体的行为取决于该方法最近的一次代理行为
  • mock对象的代理方法,允许多次调用,只要不覆盖它的代理行为,那么每次调用的执行相同的行为或者返回相同的值
  • 相同的方法和参数唯一确认一个代理。比如你可以分别代理get(int)方法在参数分别为0和1时的不同行为

@MockBean

可以在Spring ApplicationContext容器内Mock Bean。做了@MockBean标注的Mock对象会通过类型或名字(若注解中指定名字)注入到Spring容器中。容器中定义的任何类型的单例的bean将会被mock替代(若容器中不存在,则会新建)。

@Spy

与@Mock不同的是,@Spy标注的mock对象是基于真实的实例创建,对mock对象的方法调用同时也会调用真实对象的方法;

@Test
public void whenSpyingOnList_thenCorrect() {
    List list = new ArrayList();
    List 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 list = new ArrayList();
    List 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 hash = new HashMap();
    Answer answer = new Answer() {  
       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 answer = new Answer() {  
       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 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实例。
      Mockito初探——快速入门_第1张图片

    进阶——PowerMock

    Mockito可以实现常规的一些测试需求,如果有很难甚至无法测试的测试问题,可以考虑PowerMock。PowerMock可以模拟静态方法,删除静态初始化程序,允许模拟而不依赖于注入,等等。PowerMock通过在执行测试时在运行时修改字节码来完成这些技巧。PowerMock还包含一些实用程序,可以更轻松地访问对象的内部状态。

    参考

    mockito wiki
    Mockito 源码解析
    Mockito Spy 用法
    Mockito测试
    Mockito使用指南

    你可能感兴趣的:(JAVA,测试,Mock,Mockito)