Java Mock测试(Mockito入门和用法详细指南)

本文概述

  • 前言
  • 一个经典的TDD开发例子
  • Mock和Stub测试的区别?
  • 使用Mock测试有什么优点和缺点?
  • Mockito的用法
  • Stubbing
  • 总结
前言
Java Mock测试(Mockito入门和用法详细指南)

文章图片
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测试更好的重要原因。
【Java Mock测试(Mockito入门和用法详细指南)】而缺少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一个请求进行验证。

    推荐阅读