定义
mock是在测试过程中,对于一些不容易构造/获取的对象,创建一个mock对象来模拟对象的行为。比如说你需要调用B服务,可是B服务还没有开发完成,那么你就可以将调用B服务的那部分给Mock掉,并编写你想要的返回结果。 Mock有很多的实现框架,例如Mockito、EasyMock、Jmockit、PowerMock、Spock等等,SpringBoot默认的Mock框架是Mockito,和junit一样,只需要依赖spring-boot-starter-test就可以了。
Mock框架
| EasyMock | JMock | Mockito | PowerMockito | |
|---|---|---|---|---|
| final方法 | 不支持 | 不支持 | 不支持 | 支持 | 
| 私有方法 | 不支持 | 不支持 | 不支持 | 支持 | 
| 静态方法 | 不支持 | 不支持 | 不支持 | 支持 | 
| SpringBoot依赖 | 实现较为复杂 | 实现较为复杂 | 默认依赖 | 基于Mockito扩展 | 
| API风格 | 略复杂 | 略复杂 | 简单 | 简单 | 
Mockito
中文文档: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);
}