定义
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);
}