Java单元测试框架PowerMock技巧
本文章转自:乐字节
文章主要讲解:PowerMock
获取更多JAVA相关资料可以关注公众号《乐字节》 发送:999
## 前言
高德的技术大佬在谈论方法论时说到:“复杂的问题要简单化,简单的问题要深入化。”
这句话让我感触颇深,这何尝不是一套编写代码的方法——把一个复杂逻辑拆分为许多简单逻辑,然后把每一个简单逻辑进行深入实现,最后把这些简单逻辑整合为复杂逻辑,总结为八字真言即是“化繁为简,由简入繁”。
编写Java单元测试用例,其实就是把“复杂的问题要简单化”——即把一段复杂的代码拆解成一系列简单的单元测试用例;写好Java单元测试用例,其实就是把“简单的问题要深入化”——即学习一套方法、总结一套模式并应用到实践中。这里,作者根据日常的工作经验,总结了一些Java单元测试技巧,以供大家交流和学习。
## 1. 准备环境
PowerMock是一个扩展了其它如EasyMock等mock框架的、功能更加强大的框架。PowerMock使用一个自定义类加载器和字节码操作来模拟静态方法、构造方法、final类和方法、私有方法、去除静态初始化器等等。
### 1.1. 引入PowerMock包
为了引入PowerMock包,需要在pom.xml文件中加入下列maven依赖:
![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d99e3d9ab22744f09a13c32964199ab9~tplv-k3u1fbpfcp-watermark.image)
### 1.2. 集成SpringMVC项目
在SpringMVC项目中,需要在pom.xml文件中加入JUnit的maven依赖:
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d63cbf2bf97e49a09bdd055722437550~tplv-k3u1fbpfcp-watermark.image)
### 1.3. 集成SpringBoot项目
在SpringBoot项目中,需要在pom.xml文件中加入JUnit的maven依赖:
![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/be46a188ec5f4f22a45e05435827fb00~tplv-k3u1fbpfcp-watermark.image)
### 1.4. 一个简单的测试用例
这里,用List举例,模拟一个不存在的列表,但是返回的列表大小为100。
![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9d5529f2ae9a45f586a327610a6d4efc~tplv-k3u1fbpfcp-watermark.image)
## 2. mock语句
### 2.1. mock方法
声明:
T PowerMockito.mock(Class clazz);
用途:
可以用于模拟指定类的对象实例。
当模拟非final类(接口、普通类、虚基类)的非final方法时,不必使用@RunWith和@PrepareForTest注解。当模拟final类或final方法时,必须使用@RunWith和@PrepareForTest注解。注解形如:
@RunWith(PowerMockRunner.class)
@PrepareForTest({TargetClass.class})
### 2.1.1. 模拟非final类普通方法
![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/165ab3edb8014ec899443eb0ba1bfa06~tplv-k3u1fbpfcp-watermark.image)
### 2.1.2. 模拟final类或final方法
![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e5d4df73a8b34430a2d795772b05eaf6~tplv-k3u1fbpfcp-watermark.image)
### 2.2. mockStatic方法
声明:
PowerMockito.mockStatic(Class clazz);
用途:
可以用于模拟类的静态方法,必须使用“@RunWith”和“@PrepareForTest”注解。
![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5f3c5f5821aa4dbc8c18fed079d826f9~tplv-k3u1fbpfcp-watermark.image)
## 3. spy语句
如果一个对象,我们只希望模拟它的部分方法,而希望其它方法跟原来一样,可以使用PowerMockito.spy方法代替PowerMockito.mock方法。于是,通过when语句设置过的方法,调用的是模拟方法;而没有通过when语句设置的方法,调用的是原有方法。
### 3.1. spy类
声明:
PowerMockito.spy(Class clazz);
用途:
用于模拟类的部分方法。
案例:
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/43e028a0f2304155bb5f6713f9ef08e8~tplv-k3u1fbpfcp-watermark.image)
### 3.2. spy对象
声明:
T PowerMockito.spy(T object);
用途:
用于模拟对象的部分方法。
案例:
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5549c139f2d543e181ca53aa5f93e3e0~tplv-k3u1fbpfcp-watermark.image)
## 4. when语句
### 4.1. when().thenReturn()模式
声明:
![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0df74fd40c444a50b9ced5cebf216add~tplv-k3u1fbpfcp-watermark.image)
用途:
用于模拟对象方法,先执行原始方法,再返回期望的值、异常、应答,或调用真实的方法。
### 4.1.1. 返回期望值
![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/150cafdcb3b240ed9bc32a9a57c1d7f9~tplv-k3u1fbpfcp-watermark.image)
### 4.1.2. 返回期望异常
![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6042ce5c25b94cef9d91637fce659b99~tplv-k3u1fbpfcp-watermark.image)
### 4.1.3. 返回期望应答
![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7aa8b15f2d5e46fabc0eff52d35244ab~tplv-k3u1fbpfcp-watermark.image)
### 4.1.4. 调用真实方法
![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1ff6c879e6e249c9ba31764055b3b523~tplv-k3u1fbpfcp-watermark.image)
### 4.2. doReturn().when()模式
声明:
![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8fdd44d8f65446d7944d50fee4f3257f~tplv-k3u1fbpfcp-watermark.image)
用途:
用于模拟对象方法,直接返回期望的值、异常、应答,或调用真实的方法,无需执行原始方法。
注意:
千万不要使用以下语法:
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d4d319313e7248098b02572124f3f334~tplv-k3u1fbpfcp-watermark.image)
虽然不会出现编译错误,但是在执行时会抛出UnfinishedStubbingException异常。
### 4.2.1. 返回期望值
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b82d4bae95504290b1404703ef19a652~tplv-k3u1fbpfcp-watermark.image)
### 4.2.2. 返回期望异常
![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0c330d60f7d24a1493705931fbdc022d~tplv-k3u1fbpfcp-watermark.image)
### 4.2.3. 返回期望应答
![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b369b7a677ac450885b42663c01ccf8b~tplv-k3u1fbpfcp-watermark.image)
### 4.2.4. 模拟无返回值
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/542fae382309438c94b5b464b1dd552a~tplv-k3u1fbpfcp-watermark.image)
### 4.2.5. 调用真实方法
![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0830dd0f6abc4b34bd8ff15bd22613fd~tplv-k3u1fbpfcp-watermark.image)
### 4.3. 两种模式的主要区别
两种模式都用于模拟对象方法,在mock实例下使用时,基本上是没有差别的。但是,在spy实例下使用时,when().thenReturn()模式会执行原方法,而doReturn().when()模式不会执行原方法。
测试服务类:
![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/243df8620b50411fafedcdb70376fcd4~tplv-k3u1fbpfcp-watermark.image)
使用when().thenReturn()模式:
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ca682e8e1923400b96274028428ce1ae~tplv-k3u1fbpfcp-watermark.image)
在测试过程中,将会打印出"调用获取用户数量方法"日志。
使用doReturn().when()模式:
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/93c89bd1e14f4302b36ba862b47fc710~tplv-k3u1fbpfcp-watermark.image)
在测试过程中,不会打印出"调用获取用户数量方法"日志。
### 4.4. whenNew模拟构造方法
声明:
![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/98cf9638425b4bf689fab82586601cb8~tplv-k3u1fbpfcp-watermark.image)
用途:
用于模拟构造方法。
案例:
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/95880972e5dd47a4886e79a77f3b77dd~tplv-k3u1fbpfcp-watermark.image)
注意:需要加上注解@PrepareForTest({FileUtils.class}),否则模拟方法不生效。
## 5. 参数匹配器
在执行单元测试时,有时候并不关心传入的参数的值,可以使用参数匹配器。
### 5.1. 参数匹配器(any)
Mockito提供Mockito.anyInt()、Mockito.anyString、Mockito.any(Class clazz)等来表示任意值。
![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ef0572971b9a4bc4abc43ace48a71f52~tplv-k3u1fbpfcp-watermark.image)
### 5.2. 参数匹配器(eq)
当我们使用参数匹配器时,所有参数都应使用匹配器。如果要为某一参数指定特定值时,就需要使用Mockito.eq()方法。
![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e918748174aa4348b6f0cc2c57eef122~tplv-k3u1fbpfcp-watermark.image)
### 5.3. 附加匹配器
Mockito的AdditionalMatchers类提供了一些很少使用的参数匹配器,我们可以进行参数大于(gt)、小于(lt)、大于等于(geq)、小于等于(leq)等比较操作,也可以进行参数与(and)、或(or)、非(not)等逻辑计算等。
![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5c60ad3025fa4d909f4f8c46f4de2346~tplv-k3u1fbpfcp-watermark.image)
## 6. verify语句
验证是确认在模拟过程中,被测试方法是否已按预期方式与其任何依赖方法进行了交互。
格式:
Mockito.verify(mockObject[,times(int)]).someMethod(somgArgs);
用途:
用于模拟对象方法,直接返回期望的值、异常、应答,或调用真实的方法,无需执行原始方法。
案例:
### 6.1. 验证调用方法
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7b9bf722d6cf424f93c411e02b667251~tplv-k3u1fbpfcp-watermark.image)
### 6.2. 验证调用次数
![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3d83431cd39f45bbbe2f777c56cba2bf~tplv-k3u1fbpfcp-watermark.image)
除times外,Mockito还支持atLeastOnce、atLeast、only、atMostOnce、atMost等次数验证器。
### 6.3. 验证调用顺序
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7da9863969ab44c3873a613242a4c5b3~tplv-k3u1fbpfcp-watermark.image)
### 6.4. 验证调用参数
![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1aeb3b2fc3584e478f45e12382869491~tplv-k3u1fbpfcp-watermark.image)
### 6.5. 确保验证完毕
Mockito提供Mockito.verifyNoMoreInteractions方法,在所有验证方法之后可以使用此方法,以确保所有调用都得到验证。如果模拟对象上存在任何未验证的调用,将会抛出NoInteractionsWanted异常。
![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/75625d6cca8f420bbcfe5e95c80ab64f~tplv-k3u1fbpfcp-watermark.image)
备注:Mockito.verifyZeroInteractions方法与Mockito.verifyNoMoreInteractions方法相同,但是目前已经被废弃。
### 6.6. 验证静态方法
Mockito没有静态方法的验证方法,但是PowerMock提供这方面的支持。
![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a72cc5a1bc7949bc89008fef4c5576cb~tplv-k3u1fbpfcp-watermark.image)
## 7. 私有属性
### 7.1. ReflectionTestUtils.setField方法
在用原生JUnit进行单元测试时,我们一般采用ReflectionTestUtils.setField方法设置私有属性值。
![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a7104719155e41e6b6b231d28b2b62a3~tplv-k3u1fbpfcp-watermark.image)
注意:在测试类中,UserService实例是通过@Autowired注解加载的,如果该实例已经被动态代理,ReflectionTestUtils.setField方法设置的是代理实例,从而导致设置不生效。
### 7.2. Whitebox.setInternalState方法
现在使用PowerMock进行单元测试时,可以采用Whitebox.setInternalState方法设置私有属性值。
![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/83905cc5c81a486d96f7fab80bf6de4b~tplv-k3u1fbpfcp-watermark.image)
注意:需要加上注解@RunWith(PowerMockRunner.class)。
## 8. 私有方法
### 8.1. 模拟私有方法
### 8.1.1. 通过when实现
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/618abc692c4d4431ade75fc3ffcfe787~tplv-k3u1fbpfcp-watermark.image)
### 8.1.2. 通过stub实现
通过模拟方法stub(存根),也可以实现模拟私有方法。但是,只能模拟整个方法的返回值,而不能模拟指定参数的返回值。
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4faeab13db0b480e8a18275de0ee15eb~tplv-k3u1fbpfcp-watermark.image)
### 8.2. 测试私有方法
![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/619f0dc13f0446b9b5575b33b8de305b~tplv-k3u1fbpfcp-watermark.image)
### 8.3. 验证私有方法
![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d6640374758945d7adcea86dd4d38f21~tplv-k3u1fbpfcp-watermark.image)
这里,也可以用Method那套方法进行模拟和验证方法。
## 9. 主要注解
PowerMock为了更好地支持SpringMVC/SpringBoot项目,提供了一系列的注解,大大地简化了测试代码。
### 9.1. @RunWith注解
@RunWith(PowerMockRunner.class)
指定JUnit 使用 PowerMock 框架中的单元测试运行器。
### 9.2. @PrepareForTest注解
@PrepareForTest({ TargetClass.class })
当需要模拟final类、final方法或静态方法时,需要添加@PrepareForTest注解,并指定方法所在的类。如果需要指定多个类,在{}中添加多个类并用逗号隔开即可。
### 9.3. @Mock注解
@Mock注解创建了一个全部Mock的实例,所有属性和方法全被置空(0或者null)。
### 9.4. @Spy注解
@Spy注解创建了一个没有Mock的实例,所有成员方法都会按照原方法的逻辑执行,直到被Mock返回某个具体的值为止。
注意:@Spy注解的变量需要被初始化,否则执行时会抛出异常。
### 9.5. @InjectMocks注解
@InjectMocks注解创建一个实例,这个实例可以调用真实代码的方法,其余用@Mock或@Spy注解创建的实例将被注入到用该实例中。
![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a7573b81092e4f8f813579a935230c15~tplv-k3u1fbpfcp-watermark.image)
### 9.6. @Captor注解
@Captor注解在字段级别创建参数捕获器。但是,在测试方法启动前,必须调用MockitoAnnotations.openMocks(this)进行初始化。
![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/27b1637348d34adeab523fdb318e0768~tplv-k3u1fbpfcp-watermark.image)
### 9.7. @PowerMockIgnore注解
为了解决使用PowerMock后,提示ClassLoader错误。
## 10. 相关观点
### 10.1. 《Java开发手册》规范
【强制】好的单元测试必须遵守AIR原则。说明:单元测试在线上运行时,感觉像空气(AIR)一样感觉不到,但在测试质量的保障上,却是非常关键的。好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点。
A:Automatic(自动化)
I:Independent(独立性)
R:Repeatable(可重复)
【强制】单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元测试中不准使用System.out来进行人肉验证,必须使用assert来验证。
【强制】单元测试是可以重复执行的,不能受到外界环境的影响。
说明:单元测试通常会被放到持续集成中,每次有代码check in时单元测试都会被执行。如果单测对外部环境(网络、服务、中间件等)有依赖,容易导致持续集成机制的不可用。
正例:为了不受外界环境影响,要求设计代码时就把SUT的依赖改成注入,在测试时用spring 这样的DI框架注入一个本地(内存)实现或者Mock实现。
【推荐】编写单元测试代码遵守BCDE原则,以保证被测试模块的交付质量。
B:Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等。
C:Correct,正确的输入,并得到预期的结果。
D:Design,与设计文档相结合,来编写单元测试。
E:Error,强制错误信息输入(如:非法数据、异常流程、业务允许外等),并得到预期的结果。
### 10.2. 为什么要使用Mock?
根据网络相关资料,总结观点如下:
Mock可以用来解除外部服务依赖,从而保证了测试用例的独立性。
现在的互联网软件系统,通常采用了分布式部署的微服务,为了单元测试某一服务而准备其它服务,存在极大的依耐性和不可行性。
Mock可以减少全链路测试数据准备,从而提高了编写测试用例的速度。
传统的集成测试,需要准备全链路的测试数据,可能某些环节并不是你所熟悉的。最后,耗费了大量的时间和经历,并不一定得到你想要的结果。现在的单元测试,只需要模拟上游的输入数据,并验证给下游的输出数据,编写测试用例并进行测试的速度可以提高很多倍。
Mock可以模拟一些非正常的流程,从而保证了测试用例的代码覆盖率。
根据单元测试的BCDE原则,需要进行边界值测试(Border)和强制错误信息输入(Error),这样有助于覆盖整个代码逻辑。在实际系统中,很难去构造这些边界值,也能难去触发这些错误信息。而Mock从根本上解决了这个问题:想要什么样的边界值,只需要进行Mock;想要什么样的错误信息,也只需要进行Mock。
Mock可以不用加载项目环境配置,从而保证了测试用例的执行速度。
在进行集成测试时,我们需要加载项目的所有环境配置,启动项目依赖的所有服务接口。往往执行一个测试用例,需要几分钟乃至几十分钟。采用Mock实现的测试用例,不用加载项目环境配置,也不依赖其它服务接口,执行速度往往在几秒之内,大大地提高了单元测试的执行速度。
### 10.3. 单元测试与集成测试的区别
在实际工作中,不少同学用集成测试代替了单元测试,或者认为集成测试就是单元测试。这里,总结为了单元测试与集成测试的区别:
测试对象不同
单元测试对象是实现了具体功能的程序单元,集成测试对象是概要设计规划中的模块及模块间的组合。
测试方法不同
单元测试中的主要方法是基于代码的白盒测试,集成测试中主要使用基于功能的黑盒测试。
测试时间不同
集成测试要晚于单元测试。
测试内容不同
单元测试主要是模块内程序的逻辑、功能、参数传递、变量引用、出错处理及需求和设计中具体要求方面的测试;而集成测试主要验证各个接口、接口之间的数据传递关系,及模块组合后能否达到预期效果。
**感谢大家的认同与支持,小编会持续转发《乐字节》优质文章**