在实际项目中写单元测试的过程中我们会发现需要测试的类有很多依赖,这些依赖项又会有依赖,导致在单元测试代码里几乎无法完成构建,尤其是当依赖项尚未构建完成时会导致单元测试无法进行。为了解决这类问题我们引入了Mock的概念,简单的说就是模拟这些需要构建的类或者资源,提供给需要测试的对象使用。业内的Mock工具有很多,也已经很成熟了,这里我们将直接使用最流行的Mockito进行实战演练,完成mockito教程。
Mock工具概述
1.1 Mockito简介
EasyMock 以及 Mockito 都因为可以极大地简化单元测试的书写过程而被许多人应用在自己的工作中,但是这两种 Mock 工具都不可以实现对静态函数、构造函数、私有函数、Final 函数以及系统函数的模拟,但是这些方法往往是我们在大型系统中需要的功能。
另外,关于更多Mockito2.0新特性,参考官方介绍文档,里边有关于为什么不mock private的原因,挺有意思的:
https://github.com/mockito/mockito/wiki/What%27s-new-in-Mockito-2
1.2 Mockito准备工作
###Maven###
通过Maven管理的,需要在项目的Pom.xml中增加如下的依赖:
<dependencies>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.7.19</version>
<scope>test</scope>
</dependency>
</dependencies>
在程序中可以import org.mockito.Mockito,然后调用它的static方法。
Maven用户可以声明对mockito-core的依赖。 Mockito自动发布到Bintray的中心,并同步到Maven Central Repository。
特别提醒:使用手工依赖关系管理的Legacy构建可以使用1. *“mockito-all”分发。 它可以从Mockito的Bintray存储库或Bintray的中心下载。 在但是Mockito 2. * “mockito-all”发行已经停止,Mockito 2以上版本使用“mockito-core”。
官网下载中心:
http://search.maven.org/#search|gav|1|g%3A%22org.mockito%22%20AND%20a%3A%22mockito-core%22
目前最新版本为2.7.19,由于公司网络网关问题,最好是去官网手工下载。
另外Mockito需要Junit配合使用,在Pom文件中同样引入:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
然后为了使代码更简洁,最好在测试类中导入静态资源,还有为了使用常用的junit关键字,也要引入junit的两个类Before和Test:
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;
1.3 模拟对象
创建 Mock 对象的语法为 mock(class or interface)。
Mock 对象的创建
mock(Class classToMock);
mock(Class classToMock, String name)
mock(Class classToMock, Answer defaultAnswer)
mock(Class classToMock, MockSettings mockSettings)
mock(Class classToMock, ReturnValues returnValues)
可以对类和接口进行mock对象的创建,创建时可以为mock对象命名。对mock对象命名的好处是调试的时候容易辨认mock对象。
Mock对象的期望行为和返回值设定
假设我们创建了LinkedList类的mock对象:
LinkedList mockedList = mock(LinkedList.class);
1.4 设置对象调用的预期返回值
通过 when(mock.someMethod()).thenReturn(value) 来设定 Mock 对象某个方法调用时的返回值。我们可以看看源码中关于thenReturn方法的注释:
使用when(mock.someMethod()).thenThrow(new RuntimeException) 的方式来设定当调用某个方法时抛出的异常。
以及Answer:
Answer 是个泛型接口。到调用发生时将执行这个回调,通过 Object[] args = invocation.getArguments();可以拿到调用时传入的参数,通过 Object mock = invocation.getMock();可以拿到mock对象。
有些方法可能接口的参数为一个Listener参数,如果我们使用Answer打桩,我们就可以获取这个Listener,并且在Answer函数中执行对应的回调函数,这对我们了解函数的内部执行过成有很大的帮助。
使用doThrow(new RuntimeException(“clear exception”)).when(mockedList).clear();mockedList.clear();的方式Mock没有返回值类型的函数:
doThrow(new RuntimeException()).when(mockedList).clear();
//将会 抛出 RuntimeException:
mockedList.clear();
这个实例表示当执行到mockedList.clear()时,将会抛出RuntimeException。其他的doXXX执行与它类似。
例如 : doReturn()|doThrow()| doAnswer()|doNothing()|doCallRealMethod() 系列方法。
Spy函数:
你可以为真实对象创建一个监控(spy)对象,当你使用这个spy对象时,真实的对象也会被调用,除非它的函数被打桩。你应该尽量少的使用spy对象,使用时也需要小心,例如spy对象可以用来处理遗留代码,Spy示例如下:
List list = new LinkedList();
//监控一个真实对象
List spy = spy(list);
//你可以为某些函数打桩
when(spy.size()).thenReturn(100);
//使用这个将调用真实对象的函数
spy.add("one");
spy.add("two");
//打印"one"
System.out.println(spy.get(0));
//size() 将打印100
System.out.println(spy.size());
//交互验证
verify(spy).add("one");
verify(spy).add("two");
理解监控真实对象非常重要,有时,在监控对象上使用when(Object)来进行打桩是不可能或者不切实际的。因为,当使用监控对象时,请考虑用doReturn、Answer、Throw()函数组来进行打桩,例如:
List list = new LinkedList();
List spy = spy(list);
//这是不可能的: 因为调用spy.get(0)时会调用真实对象的get(0)函数,此时会发生
//IndexOutOfBoundsException异常,因为真实对象是空的 when(spy.get(0)).thenReturn("foo");
//你需要使用 doReturn() 来打桩
doReturn("foo").when(spy).get(0);
Mockito并不会为真实的对象代理函数调用,实际上它会复制真实对象,因此,如果你保留了真实对象并且与之交互,不要期望监控对象得到正确的结果。当你在监控对象上调用一个没有stub函数时,并不会调用真实对象的对应函数,你不会在真实对象上看到任何效果。
1.5 验证被测试类方法
Mock 对象一旦建立便会自动记录自己的交互行为,所以我们可以有选择的对它的 交互行为进行验证。在 Mockito 中验证 Mock 对象交互行为的方法是 verify(mock).someMethod(…)。最后 Assert() 验证返回值是否和预期一样。
1.6 Demo
从网上找来一个最简单的代码实例,下面以具体代码演示如何使用Mockito,代码有三个类,分别如下:
Person类:
public class Person {
private final int id;
private final String name;
public Person(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
}
PersonDao类:
public interface PersonDao {
Person getPerson(int id);
boolean update(Person person);
}
PersonService类:
public class PersonService {
private final PersonDao personDao;
public PersonService(PersonDao personDao) {
this.personDao = personDao;
}
public boolean update(int id, String name) {
Person person = personDao.getPerson(id);
if (person == null)
{ return false; }
Person personUpdate = new Person(person.getId(), name);
return personDao.update(personUpdate);
}
}
仍然使用Junit自动生成测试类或者手工新建测试类:
测试代码生成后,将默认assertfail的删掉,输入以下两个测试方法:
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
public class PersonServiceTest {
private PersonDao mockDao;
private PersonService personService;
@Before
public void setUp() throws Exception {
//模拟PersonDao对象
mockDao = mock(PersonDao.class);
when(mockDao.getPerson(1)).thenReturn(new Person(1, "Person1"));
when(mockDao.update(isA(Person.class))).thenReturn(true);
personService = new PersonService(mockDao);
}
@Test
public void testUpdate() throws Exception {
boolean result = personService.update(1, "new name");
assertTrue("must true", result);
//验证是否执行过一次getPerson(1)
verify(mockDao, times(1)).getPerson(eq(1));
//验证是否执行过一次update
verify(mockDao, times(1)).update(isA(Person.class));
}
@Test
public void testUpdateNotFind() throws Exception {
boolean result = personService.update(2, "new name");
assertFalse("must true", result);
//验证是否执行过一次getPerson(1)
verify(mockDao, times(1)).getPerson(eq(1));
//验证是否执行过一次update
verify(mockDao, never()).update(isA(Person.class));
}
}
注意:我们对PersonDAO进行mock,并且设置stubbing,stubbing设置如下:
- 当getPerson方法传入1的时候,返回一个Person对象,否则默认返回空
- 当调update方法的时候,返回true
这里使用了两个参数匹配器:
isA():Object argument that implements the given class.
eq():int argument that is equal to the given value
注:Mockito使用verify去校验方法是否被调用,然后使用isA和eq这些内置的参数匹配器可以更加灵活,
关于参数匹配器的详细使用请参考官网文档:https://static.javadoc.io/org.mockito/mockito-core/2.25.0/org/mockito/ArgumentMatchers.html
由于官网的代码和解释非常详细,此处就不再赘述。
仍然调用Junit执行单元测试代码,结果如图所示:
验证了两种情况:
- 更新id为1的Person的名字,预期:能在DAO中找到Person并更新成功
- 更新id为2的Person的名字,预期:不能在DAO中找到Person,更新失败
这里也可以查看Eclipse抛出的异常信息:
Argument(s) are different! Wanted:
personDao.getPerson(1);
-> at PersonServiceTest.testUpdateNotFind(PersonServiceTest.java:41)
Actual invocation has different arguments:
personDao.getPerson(2);
-> at PersonService.update(PersonService.java:8)
2 单元测试与覆盖率
1、Junit 2、JaCoCo 3、EclEmma
2 覆盖率
覆盖率如下图显示:
覆盖率仍然使用JaCoCo和EclEmma:
l 未覆盖代码标记为红色
l 已覆盖代码会标记为绿色
l 部分覆盖的代码标记为黄色
颜色也可以在Eclipse中自定义设置:
在Eclipse下方的状态栏窗口,有一栏“Coverage”,点击可以显示详细的代码覆盖率:
如何导出为Html格式的Report:
在Eclipse下方的Coverage栏鼠标右键选择“Export Session…”,在弹出窗口中选择export的目标为“Coverage Report”如下图:
点击“Next”按钮后,在接下来的弹出窗口选择需要导出的session,Format
类型选择“HTML report”,导出位置暂时选择为桌面,都选择之后点击“Finish”按钮就生成好了。
在桌面上找到一个叫做index.html的页面就是刚刚生成好的Coverage Report:
点击文件夹可以进入目录,进一步查看子文件的覆盖率:
附录:参考文档一览
Mockito官网: http://site.mockito.org/
5分钟了解Mockito:http://liuzhijun.iteye.com/blog/1512780
Mockito简单介绍及示例:http://blog.csdn.net/huoshuxiao/article/details/6107835
Mockito浅谈:http://www.jianshu.com/p/77db26b4fb54
单元测试利器-Mockito 中文文档:http://blog.csdn.net/bboyfeiyu/article/details/52127551
Mockito使用指南 :http://blog.csdn.net/shensky711/article/details/52771493
JUnit+Mockito 单元测试(二):http://blog.csdn.net/zhangxin09/article/details/42422643
感谢阅读!作者原创技术文章,转载请注明出处
看完点个赞呗,难道想白嫖不成?更多内容请访问微信公众号 :三国测,扫码关注哟!
其他推荐相关阅读:
单元测试系列之一:如何使用JUnit、JaCoCo和EclEmma提高单元测试覆盖率
单元测试系列之四:Sonar平台中项目主要指标以及代码坏味道详解