本文概述
前言
Mockito是一个非常简单的Java Mock框架,其实类似其它的Mock框架都非常简单。我认为并没有使用Mock框架的必要性!如果你正在以TDD的模式进行开发,尽快开发就是了。至于Mock框架,还是得看情况使用。
我认为不懂Mock测试的作用或试图大量使用Mock对象,使用Mock框架是一个灾难——千万不要大量使用Mock对象,这会相比于经典TDD开发模式——使用实际对象,代码质量极速下降。所以并不是多用Mock就好了,这是个假的对象,泛滥使用感觉更像是自己骗自己。
不过Mock测试仍然是单元测试中的一个重点内容,而且和TDD开发相关,下面我们一起讨论下Mock测试和Mockito框架的使用。
一个经典的TDD开发例子
习惯上,使用TDD开发首先要看需求文档,针对特定需求编写测试用例(设计需求接口)。测试用例的一般形式为:
- 初始化数据,包括创建测试的主对象和一些依赖的对象或数据。
- 执行主对象指定的方法或操作,该方法是当前测试用例的主要测试内容。
- 使用断言检查操作后的状态。
一般地,一个类A对应一个测试类a,那么这个类A称为主类,在a中测试A使用的对象称为主对象。一个测试方法对应测试主类中某个方法的一种或几种测试,当前测试方法中测试的方法称为主方法。而当前类或对象依赖的其它类或对象,又可称为次要类或方法。
当然这些名称并不是标准的,但是本文使用这种名称,以便在后面的讨论中能够清晰区分。
下面是一个经典的测试用例:
@Test
public void sampleTest() {
// 1. initialize
Respository respository = new RespositoryImp();
Service service = new ServiceImp(respository);
// 2. execute
Post post = service.getPost();
// 3. assert state
assertNotNull(post);
}
其中Service为主类,Respository为次要类,这里主要测试service的getPost方法。测试分为三个步骤:初始化Service对象、执行getPost方法、检查方法返回的状态值。
下面有几个可能的问题:
1)如果该项目的开发者同时负责开发Service和Respository,那么使用TDD开发的时候,你会发现,我不但要在getPost中实现相关逻辑,而且因为getPost还会用到Respository中的操作,这时又不得不继续向下实现Respository。
这里显示出TDD的一个问题:为了实现一个需求接口,我们需要不断往下深入实现,直到该需求实现为止。
2)如果该项目开发者只负责开发Service,或者开发者目前还不想去实现Respository。那么这个时候我们可以在test中创建一个简单的Respository实现,以辅助Service的相关测试通过,而这个简单的实现又称为Stub实现。
这种测试就称为Mock测试,就是临时创建一个次要类或次要对象辅助当前的主类测试通过。
Mock和Stub测试的区别?
Stub一般至少简单地创建一个临时对象,而不做其它事情。Mock除了创建临时对象,还包括对象的行为/方法预设和验证。
Mock测试的一般流程为:初始化 -> 设置预期 -> 执行操作 -> 验证(行为),而Stub的一般流程为:初始化 -> 执行操作 -> 验证(状态)。
这只是一种简单的区分,另外还有一些关于dummy, fake, stub, mock的区别,可以参考下stackoverflow上的讨论:https://stackoverflow.com/questions/3459287/whats-the-difference-between-a-mock-stub。
使用Mock测试有什么优点和缺点?
使用Mock的好处是,当我们使用A类来实现需求的时候,仅仅关注A类就行了,相关依赖只需mock出来就行,这样可以实现隔离测试,出现bug只需从A中找即可,这也是多数认为Mock测试更好的重要原因。
而缺少mock的经典TDD则可能要面对高耦合的问题,比如出现bug的时候,寻找bug会有点困难,但我觉得问题不大。
而它的优点又是它的缺点:看似很美好,但是很假。mock出来的对象其构造是有限的,它并没有真实对象那么灵活,至多起到辅助测试的作用。并且当分别隔离测试A类和B类的接口时,这不能保证A和B耦合起来运行多数情况都是正确的,最后你仍然需要A和B真实耦合的集成测试。
另外一个问题是,要实现高质量的代码,我们不能只对一个函数使用一个输入进行测试,比如我们需要进行路径覆盖测试,或者至少进行代码覆盖测试。而这时候则需要写多个测试,或者使用参数化测试,例如JUnit 5的@Parameterized参数化测试。这时如果我们全部都是使用mock测试,这等于花很大功夫得到比较低的代码质量。
所以,参考Mockito的官方建议“Don’t mock everything”。只有面对一些必要而又难以创建的对象,或者该次要对象非常简单,没什么大变动(但这时和创建真实对象也没大区别),这时候可以考虑Mock该对象,例如HTTP中的Request、Response等。
最好,要记住:就像是用Mock的理由那样——“真实对象具有不确定的行为,产生不可预测的效果”,问题是我们的项目就是要应对真实情况的,而Mock出来的对象多少有些问题,所以除非很有必要,否则不用都没关系。
Mockito的用法
Mock测试主要是Mock当前被测试类的相关依赖,而不是Mock当前被测试类,所以不要看到什么就Mock一下。
一般的TDD用例测试包括:初始化、运行、验证,而Mock测试则包括:Stub和验证。
验证行为
验证行为的意思是:mock一个对象出来,然后验证这个对象的相关方法是否执行过。Mock对象是辅助主类通过测试的,而验证mock对象的行为主要是期望当前被测试的对象使用过mock对象的一些操作。
/**
* 验证行为
* */
@Test
public void sampleTest() {
List<String> list = mock(List.class);
list.add("111");
list.add("222");
list.remove(0);
verify(list).add("111");
verify(list).add("222");
verify(list).remove(0);
}
Stubbing
Stub需要在被测试方法实际执行之前创建,它相当于预定义Mock对象的操作结果,例如操作返回值以及操作抛出的异常。
使用语法一般为:when(action()).thenReturn(),或者when(action()).thenThrow(new Exception)。
/**
* stub
* */
@Test
public void stubTest() {
Vector<String> vector = mock(Vector.class);
when(vector.get(0)).thenReturn("Stub");
when(vector.get(1)).thenReturn("Rain");
when(vector.remove(anyInt())).thenThrow(new UnsupportedOperationException());
vector.get(0);
vector.get(1);
vector.remove(0);
vector.remove(1);
verify(vector).get(0);
verify(vector).get(1);
verify(vector).remove(0);
verify(vector).remove(1);
}
Mockito的用法就这么多了,更细致的操作你可以使用代码提示即可看到。另外你可以使用@Mock注解Mock一个对象,但基本就这么多了,针对Mock对象,先mock出来,然后stub,最后验证,还是很简单的,最主要的是前面的内容,解析了我们为什么需要Mock对象。
不得不说,这些写代码还是挺舒服的,不用考虑依赖,不用测试依赖。即使如此,我还是得面对现实,使用少量Mock,追求更高的代码质量。
总结
Mock测试主要是为了解决当前类的依赖问题,我们可以使用一些Mock框架如Mockito创建这些依赖的虚拟对象,以提供给当前类测试通过。
但是对于Mock框架的使用态度是:除非必要,否则尽量少用。另外Mock有大用处的是在MVC开发中,例如在Spring Boot中开发,如果我们要验证一个API接口,那么我们需要手动打开浏览器,或者使用诸如postman的工具,或者创建HTTP请求(当然太重量了),但是最简单的是Mock一个请求进行验证。