Mock单元测试入门
Mock单元测试入门
Mock基础知识
什么是Mock
Mock 是在测试过程中,对于一些不容易构造/获取的对象,创建一个Mock 对象来模拟对象的行为。Mock 最大的功能是帮你把单元测试进行解耦,如果你的代码对另一个类或者接口有依赖,它能够帮你模拟这些依赖,并帮你验证所调用的依赖的行为。
Mock的目的
- 验证这个对象的某些方法的调用情况,调用了多少次,参数是什么,返回值是什么等等
- 指定这个对象的某些方法的行为,返回特定的值,或者是执行特定的动作等等
Mock的应用场景
-
团队并行开发:有了Mock,前、后端人员只需要定义好接口文档就可以开始并行工作,互不影响。后端与后端之间如果有接口耦合,也同样使用Mock解决。
-
开启TDD模式:即测试驱动开发。单元测试是TDD实现的基石,而TDD经常会碰到协同模块尚未开发完成的情况,但是有了mock,这些一切都不是问题。当接口定义好后,测试人员就可以创建一个Mock,把接口添加到自动化测试环境,提前创建测试。
-
隔离系统:假如需要调用一个post请求,为了获得响应,来看当前系统是否能正确处理返回的“响应”,但是这个post请求会造成数据库中数据的污染,那么就可以充分利用Mock,构造一个虚拟的post请求,指定返回的数据就OK。
-
模拟访问资源:比如说,你需要调用一个“墙”外的资源来方便自己调试,就可以自己Mock一个。
-
系统演示:假如我们需要创建一个演示系统,并且做了简单的UI,那么在完全没有开发后端服务的情况下,也可以进行演示。
-
测试场景
-
接口测试:对一个接口进行测试,排除这个接口对外部环境的依赖,避免因外部资源而导致测试失败。
-
功能测试:在功能测试初期,我们往往依赖到第三方提供的接口,但是第三方接口初期是不稳定的(虽然前期已做好服务接口的约定),后期当有较稳定的接口后,我们进行全面地测试,则接口的功能都正常了。如果数据准备比较麻烦或者造数据要双方配合成本比较高的项目,在对外部接口进行第一次全面联调测试后,仍然可以考虑使用mock来全面测试自己内部的功能逻辑。
-
UI自动化测试:对数据源进行mock,UI自动化测试时,页面上展示的信息是来源于其他的数据源。
-
测试覆盖度:假如有一个接口,有100个不同类型的返回,我们需要测试它在不同返回类型下,系统是否能够正常响应,但是有些返回在正常情况下基本不会发生,难道你要千方百计给系统做各种手脚让它返回以便测试吗?比如,我们需要测试在当接口发生500错误的时候,app是否崩溃,别告诉我你一定要给服务端代码做些手脚让它返回500 。而使用mock,这一切就都好办了,想要什么返回就模拟什么返回,妈妈再也不用担心我的测试覆盖度了!
Mock框架对比(JAVA)
框架 | 简介 | 说明 |
---|---|---|
EasyMock | EasyMock 是早期比较流行的MocK测试框架。它提供对接口的模拟,能够通过录制、回放、检查三步来完成大体的测试过程,可以验证方法的调用种类、次数、顺序,可以令 Mock 对象返回指定的值或抛出指定异常。通过 EasyMock,我们可以方便的构造 Mock 对象从而使单元测试顺利进行。 | |
Mockito | EasyMock之后流行的mock工具。它与EasyMock和jMock很相似,但是通过在执行后校验什么已经被调用,它消除了对期望行为(expectations)的需要。其它的mocking库需要你在执行前记录期望行为(expectations),而这导致了丑陋的初始化代码。 | 相对EasyMock学习成本低,而且具有非常简洁的API,测试代码的可读性很高。 |
PowerMock | 这个工具是在EasyMock和Mockito上扩展出来的,目的是为了解决EasyMock和Mockito不能解决的问题,比如对static, final, private方法均不能mock。其实测试架构设计良好的代码,一般并不需要这些功能,但如果是在已有项目上增加单元测试,老代码有问题且不能改时,就不得不使用这些功能了。 | PowerMock在扩展功能时完全采用和被扩展的框架相同的 API, 熟悉 PowerMock 所支持的模拟框架的开发者会发现 PowerMock 非常容易上手。PowerMock 的目的就是在当前已经被大家所熟悉的接口上通过添加极少的方法和注释来实现额外的功能。目前PowerMock 仅扩展了 EasyMock 和 mockito,需要和EasyMock或Mockito配合一起使用。 |
Jmockit | JMockit 是一个轻量级的mock框架是用以帮助开发人员编写测试程序的一组工具和API,该项目完全基于 Java 5 SE 的 java.lang.instrument 包开发,内部使用 ASM 库来修改Java的Bytecode。 | Jmockit功能和PowerMock类似,某些功能甚至更为强大,但其代码的可读性并不强。 |
MockMVC | 基于RESTful风格的SpringMVC单元测试,可以测试完整的SpringMVC流程,即从URL请求到控制处理器,到视图渲染都可以测试。 | |
综合了各个工具的特性及资料丰富程度,我建议大家使用Mockito,简单实用,文档丰富。下面就Mockito这个框架进行展开说明。 |
Mockito研究
资料
- 官网:http://mockito.org
- API文档:http://docs.mockito.googlecode.com/hg/org/mockito/Mockito.html
- 项目源码:https://github.com/mockito/mockito
依赖
通过Maven管理的,需要在项目的Pom.xml中增加如下的依赖:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.10.19</version>
<scope>test</scope>
</dependency>
需要配合junit使用
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
核心注解
- @RunWith
//这是一个类级别的注释。它用于保持测试干净并改善调试。
//它还会检测测试中可用的未使用的存根, 并使用@Mock注释对模拟进行初始化。
//@RunWith批注在org.mockito.junit包中可用。以下代码段显示了如何使用@RunWith批注:
@RunWith(MockitoJUnitRunner.class)
public class ToDoBusinessMock {
......
}
- @Captor
它允许创建字段级参数捕获器。它与Mockito的verify()方法一起使用, 以获取调用方法时传递的值。
@Captor ArgumentCaptor argumentCaptor;
- @InjectMocks
//创建一个实例,简单的说是这个Mock可以调用真实代码的方法,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中。@InjectMocks
private ToDoServiceImpl toDoService;
// 注:ToDoService 不能是接口,而是一个具体实现类。
- @Mock
//Mock 对函数的调用均执行mock(即虚假函数),不执行真正部分。
@Mock
private ToDoService mockToDoService;
- @Spy
Spy对函数的调用均执行真正部分。
@Spy
private ToDoService mockToDoService;
常用方法
mock
一个对象
List list = mock(List.class);
verify
验证交互行为
// 静态导入会使代码更简洁
import static org.mockito.Mockito.*;
// mock creation 创建mock对象
List mockedList = mock(List.class);
//using mock object 使用mock对象
mockedList.add("one");
mockedList.clear();
//verification 验证
verify(mockedList).add("one");
verify(mockedList).clear();
thenReturn
模拟期望结果
List list = mock(List.class);
when(mock.get(0)).thenReturn("hello");
assertThat(mock.get(0),is("hello"));
doThrow
模拟抛出异常
List list = mock(List.class); //模拟行为
doThrow(new RuntimeException()).when(list).add(1);
//执行时抛出异常
list.add(1);
- 为程序打桩(stub)
//stubbing
LinkedList mockedList = mock(LinkedList.class);
// 输出“first”
when(mockedList.get(0)).thenReturn("first");
// 抛出异常
when(mockedList.get(1)).thenThrow(new RuntimeException());
System.out.println(mockedList.get(0));
System.out.println(mockedList.get(1));
// 因为get(999) 没有打桩,因此输出null
System.out.println(mockedList.get(999));
- 参数匹配器
// 使用内置的anyInt()参数匹配器
when(mockedList.get(anyInt())).thenReturn("element");
// 使用自定义的参数匹配器(在isValid()函数中返回你自己的匹配器实现)
when(mockedList.contains(argThat(isValid()))).thenReturn("element");
//输出element
System.out.println(mockedList.get(999));
// 你也可以验证参数匹配器
verify(mockedList).get(anyInt());
- 验证函数的确切、最少、从未调用次数
//using mock
mockedList.add("once");
mockedList.add("twice");
mockedList.add("twice");
mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");
// 下面的两个验证函数效果一样,因为verify默认验证的就是times(1)
verify(mockedList).add("once");
verify(mockedList, times(1)).add("once");
// 验证具体的执行次数
verify(mockedList, times(2)).add("twice");
verify(mockedList, times(3)).add("three times");
// 使用never()进行验证,never相当于times(0)
verify(mockedList, never()).add("never happened");
// 使用atLeast()/atMost()
verify(mockedList, atLeastOnce()).add("three times");
verify(mockedList, atLeast(2)).add("five times");
verify(mockedList, atMost(5)).add("three times");
InOrder
验证执行顺序
// A. 验证mock一个对象的函数执行顺序
List singleMock = mock(List.class);
singleMock.add("was added first");
singleMock.add("was added second");
// 为该mock对象创建一个inOrder对象
InOrder inOrder = inOrder(singleMock);
// 确保add函数首先执行的是add("was added first"),然后才是add("was added second")
inOrder.verify(singleMock).add("was added first");
inOrder.verify(singleMock).add("was added second");
//------------------------------------------------------------------
// B .验证多个mock对象的函数执行顺序
List firstMock = mock(List.class);
List secondMock = mock(List.class);
firstMock.add("was called first");
secondMock.add("was called second");
// 为这两个Mock对象创建inOrder对象
InOrder inOrder = inOrder(firstMock, secondMock);
// 验证它们的执行顺序
inOrder.verify(firstMock).add("was called first");
inOrder.verify(secondMock).add("was called second");
verifyZeroInteractions
验证零互动
@Test
public void mockTest() {
List list = mock(List.class);
List list2 = mock(List.class);
List list3 = mock(List.class);
list.add(1);
verify(list).add(1);
verify(list,never()).add(2);
//验证零互动行为
verifyZeroInteractions(list2,list3);
}
verifyNoMoreInteractions
验证冗余互动行为
@Test(expected = NoInteractionsWanted.class)
@SuppressWarnings("unchecked")
public void mockTest() {
List list = mock(List.class);
list.add(1);
list.add(2);
verify(list, times(2)).add(anyInt());
// 检查是否有未被验证的互动行为,因为add(1)和add(2)都会被上面的anyInt()验证到
// 所以下面的代码会通过
verifyNoMoreInteractions(list);
List list2 = mock(List.class);
list2.add(1);
list2.add(2);
verify(list2).add(1);
// 检查是否有未被验证的互动行为,因为add(2)没有被验证,所以下面的代码会失败抛出异常
verifyNoMoreInteractions(list2);
}
thenAnswer
为回调做测试桩
// 运行为泛型接口Answer打桩
when(mock.someMethod(anyString())).thenAnswer(new Answer() {
Object answer(InvocationOnMock invocation) {
Object[] args = invocation.getArguments();
Object mock = invocation.getMock();
return "called with arguments: " + args;
}
});
// 输出 : "called with arguments: foo" System.out.println(mock.someMethod("foo"));
reset
重置mock
List list = mock(List.class);
when(list.size()).thenReturn(10);
list.add(1);
assertThat(list.size(),is(10));
//重置mock,清除所有的互动和预设
reset(list);
assertThat(list.size(),is(0));