Mockito

young 678 2022-05-09

定义

mock是在测试过程中,对于一些不容易构造/获取的对象,创建一个mock对象来模拟对象的行为。比如说你需要调用B服务,可是B服务还没有开发完成,那么你就可以将调用B服务的那部分给Mock掉,并编写你想要的返回结果。 Mock有很多的实现框架,例如Mockito、EasyMock、Jmockit、PowerMock、Spock等等,SpringBoot默认的Mock框架是Mockito,和junit一样,只需要依赖spring-boot-starter-test就可以了。

Mock框架

EasyMockJMockMockitoPowerMockito
final方法不支持不支持不支持支持
私有方法不支持不支持不支持支持
静态方法不支持不支持不支持支持
SpringBoot依赖实现较为复杂实现较为复杂默认依赖基于Mockito扩展
API风格略复杂略复杂简单简单

Mockito

官网:https://mockito.org

中文文档:https://github.com/hehonghui/mockito-doc-zh

Mockito并不是创建一个真实的对象,而是模拟这个对象,他用简单的when(mock.method(params)).thenRetrun(result)语句设置mock对象的行为,如下语句:

// 设置mock对象的行为 - 当调用其get方法获取第0个元素时,返回"first"
Mockito.when(mockedList.get(0)).thenReturn("first");

在Mock对象的时候,创建一个proxy对象,保存被调用的方法名(get),以及调用时候传递的参数(0),然后在调用thenReturn方法时再把“first”保存起来,这样,就有了构建一个stub方法所需的所有信息,构建一个stub。当get方法被调用的时候,实际上调用的是之前保存的proxy对象的get方法,返回之前保存的数据。

使用

验证行为

@Test
public void verifyBehaviour(){
    List mock = mock(List.class);
    mock.add(1);
    mock.clear();
    verify(mock).add(1);
    verify(mock).clear();
}

模拟期望的结果

@Test
public void whenThenReturn(){
    //mock一个Iterator类
    Iterator iterator = mock(Iterator.class);
    //预设当iterator调用next()时第一次返回hello,第n次都返回world
    when(iterator.next()).thenReturn("hello").thenReturn("world");
    //使用mock的对象
    String result = iterator.next() + " " + iterator.next() + " " + iterator.next();
    //验证结果
    assertEquals("hello world world",result);
}
@Test(expected = IOException.class)
public void whenThenThrow() throws IOException {
    OutputStream outputStream = mock(OutputStream.class);
    OutputStreamWriter writer = new OutputStreamWriter(outputStream);
    //预设当流关闭时抛出异常
    doThrow(new IOException()).when(outputStream).close();
    outputStream.close();
}

RETURNS_SMART_NULLS和RETURNS_DEEP_STUBS

import static org.mockito.Mockito.mock;

@Test
public void returnSmartNullsTest(){
    List mock = mock(List.class);
    System.out.println(mock.get(0));
    System.out.println(mock.toArray().length);
}

运行此用例,发现会出现NPE

在创建mock对象时,有的方法没有进行stubbing,所以调用时会放回null,这样在进行操作的时候很可能就会抛出空指针异常。如果通过RETURNS_SMART_NULLS参数创建的mock对象,在没有调用stubbed方法时会返回SmartNull。如返回类型是String,会返回"",如果是int,会返回0,list会返回空的list。

import static org.mockito.Mockito.RETURNS_SMART_NULLS;
import static org.mockito.Mockito.mock;

@Test
public void returnSmartNullsTest(){
    List mock = mock(List.class,RETURNS_SMART_NULLS);
    System.out.println(mock.get(0));
    System.out.println(mock.toArray().length);
}

RETURNS_DEEP_STUBS参数程序会自动进行mock所需的对象

@Data
class Vo2 {
    private String destination;
}

@Data
class Vo1 {
    private Vo2 vo2;
}

正常情况下需要对内部对象都进行mock

@Test
public void deepStubsTest(){
    Vo1 vo1 =mock(Vo1.class);
    Vo2 vo2 =mock(Vo2.class);
    when(vo1.getVo2()).thenReturn(vo2);
    when(vo2.getDestination()).thenReturn("Beijing");

    vo1.getVo2().getDestination();
    verify(vo1.getVo2()).getDestination();
    assertEquals("Beijing", vo1.getVo2().getDestination());
}

使用RETURNS_DEEP_STUBS则不需要

@Test
public void deepStubsTest(){
    Vo1 vo1 =mock(Vo1.class,RETURNS_DEEP_STUBS);
    when(vo1.getVo2().getDestination()).thenReturn("Beijing");
    vo1.getVo2().getDestination();
    verify(vo1.getVo2()).getDestination();
    assertEquals("Beijing", vo1.getVo2().getDestination());
}

模拟方法体抛出异常

@Test
public void testDoThrow(){
    List list = mock(List.class);
    doThrow(new RuntimeException()).when(list).add(1);
    assertThrows(RuntimeException.class,()->list.add(1));
}

使用注解快速模拟

为了避免重复mock,让测试类更具有可读性,可以是用@Mock快速模拟

@Mock
private List list;

@Test
public void testAnnoMock(){
    list.add(1);
    verify(list).add(1);
}

运行此测试用例,会抛出NEP,mock的对象为null,需要增加初始化mock的代码。

可以在测试类的构造函数中增加初始化代码

public TestMockito() {
    MockitoAnnotations.initMocks(this);
}

@Mock
private List list;

@Test
public void testAnnoMock(){
    list.add(1);
    verify(list).add(1);
}

或者在测试类上加上注解

JUnit4:@RunWith(MockitoJUnitRunner.class)

JUnit5:@ExtendWith(MockitoExtension.class)

参数匹配

@Test
public void testMatchers() {
    List list = mock(List.class);
    when(list.get(anyInt())).thenReturn("element");
    assertEquals("element", list.get(678));
    assertEquals("element", list.get(999));
    Comparable comparable = mock(Comparable.class);
    when(comparable.compareTo("Test")).thenReturn(1);
    when(comparable.compareTo("OMG")).thenReturn(-1);
    assertEquals(1, comparable.compareTo("Test"));
    assertEquals(-1, comparable.compareTo("OMG"));
    assertEquals(0, comparable.compareTo("No Stub"));
}

也可以自定义匹配器

@Test
public void testCustomizeMatchers() {
    List list = mock(List.class);
    when(list.contains(argThat(new MyArgumentMatcher()))).thenReturn(true);
    assertEquals(true,list.contains(null));
    assertEquals(true,list.contains(1));
    assertEquals(false,list.contains(2));
}

// 自定义参数匹配器
static class MyArgumentMatcher implements ArgumentMatcher<Integer> {
    @Override
    public boolean matches(Integer argument) {
        return argument == null || 1 == argument;
    }
}

参数捕获器

@Test
public void testCapturingArgs(){
    PersonDao personDao = mock(PersonDao.class);
    PersonService personService = new PersonService(personDao);
    ArgumentCaptor<Person> argument = ArgumentCaptor.forClass(Person.class);
    personService.update(1,"young");
    verify(personDao).update(argument.capture());
    assertEquals(1,argument.getValue().getId());
    assertEquals("young",argument.getValue().getName());
}

@AllArgsConstructor
@Data
class Person {
    private int id;
    private String name;
}

interface PersonDao {
    public void update(Person person);
}

@AllArgsConstructor
class PersonService {
    private PersonDao personDao;
    public void update(int id, String name) {
        personDao.update(new Person(id, name));
    }
}

使用方法预期回调接口生成期望值(Answer结构)

@Test
public void answerTest(){
    List list = mock(List.class);
    when(list.get(anyInt())).thenAnswer(new CustomAnswer());
    assertEquals("hello world99",list.get(99));
    assertEquals("hello world2333",list.get(2333));
}

static class CustomAnswer implements Answer<String>{
    @Override
    public String answer(InvocationOnMock invocation) throws Throwable {
        Object[] arguments = invocation.getArguments();
        return "hello world" + arguments[0];
    }
}

也可以使用匿名内部类实现

@Test
public void answerTest(){
    List list = mock(List.class);
    when(list.get(anyInt())).thenAnswer((Answer<String>) invocation -> {
        Object[] arguments = invocation.getArguments();
        return "hello world" + arguments[0];
    });
    assertEquals("hello world99",list.get(99));
    assertEquals("hello world2333",list.get(2333));
}

修改对未预设的调用返回默认期望

@Test
public void testUnStubbedInvocation(){
    // mock对象使用Answer来对未预设的调用返回默认期望值
    List list = mock(List.class, new Answer() {
        @Override
        public Object answer(InvocationOnMock invocation) throws Throwable {
            return 2333;
        }
    });
    // get(1) 没有预设值,通常情况下会返回NULL,这里使用自定义Answer改变了默认期望值
    assertEquals(2333,list.get(1));
    // size() 没有预设值,通常情况下会返回0,这里使用自定义Answer改变了默认期望值
    assertEquals(2333,list.size());
}
@Test
public void testUnStubbedInvocation(){
    // mock对象使用Answer来对未预设的调用返回默认期望值
    List list = mock(List.class, new Answer() {
        @Override
        public Object answer(InvocationOnMock invocation) throws Throwable {
            Method method = invocation.getMethod();
            String methodName = method.getName();
            if ("size".equals(methodName)){
                return 9960;
            }
            if ("get".equals(methodName)){
                if (invocation.getArgument(0,Integer.class)==1){
                    return 2673;
                }
            }
            return 2333;
        }
    });
    assertEquals(2673,list.get(1));
    assertEquals(2333,list.get(2));
    assertEquals(9960,list.size());
}

使用spy监控真实对象

Mock不是真实的对象,它只是用类型的class创建了一个虚拟对象,并可以设置对象行为。

spy是一个真实的对象,但他也可以设置对象行为

@Test
public void testSpyOnRealObjects(){
    List list = new LinkedList();
    List spy = spy(list);
    // 方法会报错,因为会调用真实对象的get(0),所以会抛出数组下标越界的异常
    //  when(spy.get(0)).thenReturn(3);
    // 使用doReturn when可以避免when thenReturn 调用真实对象API
    doReturn(123).when(spy).get(233);
    when(spy.size()).thenReturn(100);
    spy.add(1);
    spy.add(2);
    assertEquals(100,spy.size());
    assertEquals(1,spy.get(0));
    assertEquals(2,spy.get(1));
    assertEquals(123,spy.get(233));
    verify(spy).add(1);
    verify(spy).add(2);
	  assertThrows(IndexOutOfBoundsException.class,()->spy.get(2));
}

真实的部分mock

@Test
public void testCallRealMethod(){
  	// 通过spy调用真实API
    List list = spy(new ArrayList<>());
    assertEquals(0, list.size());
    A a = mock(A.class);
  	// 通过thenCallRealMethod来调用真实API
    when(a.doSomething(anyInt())).thenCallRealMethod();
    assertEquals(233,a.doSomething(233));
}

重置mock

@Test
public void testResetMock(){
    List list = mock(List.class);
    when(list.size()).thenReturn(10);
    list.add(1);
    assertEquals(10,list.size());
    reset(list);
    assertEquals(0,list.size());
}

验证调用次数

@Test
public void testVerifyNumberOfInvocations(){
    List list = mock(List.class);
    list.add(1);
    list.add(2);
    list.add(2);
    list.add(3);
    list.add(3);
    list.add(3);
    // 验证是否被调用1次
    verify(list).add(1);
    verify(list,times(1)).add(1);
    // 验证是否被调用2次
    verify(list,times(2)).add(2);
    // 验证是否被调用3次
    verify(list,times(3)).add(3);
    // 验证是否从未被调用过
    verify(list,never()).add(4);
    // 验证至少调用一次
    verify(list,atLeastOnce()).add(1);
    // 验证至少调用2次
    verify(list,atLeast(2)).add(2);
    // 验证最多调用3次
    verify(list,atMost(3)).add(3);
}

测试连续调用

@Test
public void testConsecutiveCall(){
    List list = mock(List.class);
    // 模拟连续调用返回期望值,如果分开,则只有最后一个有效
    when(list.get(0)).thenReturn(0);
    when(list.get(0)).thenReturn(1);
    // get(0)返回期望值被覆盖为2
    when(list.get(0)).thenReturn(2);
    // get(1) 先返回0,在返回1,最后抛出Runtime异常
    when(list.get(1)).thenReturn(0).thenReturn(1).thenThrow(new RuntimeException());
    assertEquals(2,list.get(0));
    assertEquals(2,list.get(0));
    assertEquals(0,list.get(1));
    assertEquals(1,list.get(1));
    // 第三次或多次调用都会抛出异常
    assertThrows(RuntimeException.class,()->list.get(1));
}

验证执行顺序

@Test
public void testVerifyInOrder(){
    List list = mock(List.class);
    List list2 = mock(List.class);
    list.add(1);
    list2.add("hello");
    list.add(2);
    list2.add("world");
    // 将要验证排序的mock对象放入inOrder
    InOrder inOrder = inOrder(list, list2);
    // 验证执行顺序,如果顺序与上面执行的顺序不一直则会验证失败
    inOrder.verify(list).add(1);
    inOrder.verify(list2).add("hello");
    inOrder.verify(list).add(2);
    inOrder.verify(list2).add("world");
}

验证模拟对象无互动

@Test
public void testVerifyNoInteractions(){
    List list = mock(List.class);
    List list2 = mock(List.class);
    List list3 = mock(List.class);
    list.add(1);
    verify(list).add(1);
    // 验证list没有执行过add(2)交互
    verify(list,never()).add(2);
    // 验证在给定的模拟上没有发生任何交互
    verifyNoInteractions(list2,list3);
}

找出未被验证的交互

@Test
public void testVerifyNoInteractions(){
    List list = mock(List.class);
    list.add(1);
    list.add(2);
    verify(list,times(2)).add(anyInt());
    // anyInt包含了1和2,所以验证通过
    verifyNoMoreInteractions(list);

    List list2 = mock(List.class);
    list2.add(1);
    list2.add(2);
    verify(list2).add(1);
    // add(2)行为未被验证,所以验证失败
    verifyNoMoreInteractions(list2);
}