概述

由于最近工作需要, 在项目中要做单元测试, 以达到指定的测试用例覆盖率指标。项目中我们引入的powermockito来编写测试用例, JaCoCo来监控单元测试覆盖率。关于框架的选择, 网上讨论mockito和powermockito孰优孰劣的文章众多, 这里就不多做阐述, 读者如有兴趣可自行了解。

依赖引入

<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4-rule-agent</artifactId>
<version>1.6.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito</artifactId>
<version>1.6.6</version>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>1.6.6</version>
</dependency>

被测试类

public class PowerMockitoDemo {

    @Autowired
private StudentDao studentDao; @Autowired
private TeacherService teacherService; public void study() {
//doSomething
} private void play(Map<String, Object> project, Person person, int hours) {
//doSomething
} private boolean updateStudentName(String newName) {
//doSomething
} public ServiceResult grantRights(List<String> usernames, String rights, String orderid, String id) {
String value1 = PropertiesUtil.get("key1");
String value2 = PropertiesUtil.get("key2");
studentDao.saveRecord(usernames);//返回值类型void
Student student = studentDao.getStudentById(id);
boolean result = this.verifyParams(usernames);
teacherService.syncDB2Redis(usernames, orderid);
this.updateOperation(rights);
} private boolean verifyParams(List<String> usernames) {
//doSomething
} private void updateOperation(String rights) {
//doSomething
}
}

测试用例基类

//@PrepareForTest注解和@RunWith注解需结合使用,单独使用将不起作用
@RunWith(PowerMockRunner.class)
@PrepareForTest({RedisUtils.class})
@SuppressStaticInitializationFor({"com.test.util.RedisUtils", "com.test.util.HttpUtils"})//用于阻止类中的静态代码块执行
public abstract class BaseTest { public RedisUtils redisUtils; @Rule
public ExpectedException thrown = ExpectedException.none();//断言要抛出的异常 public void setUp() {
initMocks(this); PowerMockito.suppress(PowerMockito.constructor(ShardedJedisClientImpl.class, String.class));
redisUtils = PowerMockito.mock(RedisUtils.class);
Whitebox.setInternalState(RedisUtils.class, "redisUtil", redisUtils);//给类或实例对象的成员变量设置模拟值,这里是给RedisUtils类中的字段redisUtil设置模拟值 PowerMockito.suppress(PowerMockito.constructor(HttpUtils.class));
PowerMockito.mockStatic(HttpUtils.class);//mock类中所有静态方法
} /**
* @param instance 真实对象
* @param methodName 方法名
* @param args 形参列表
*/
public Object callPrivateMethod(Object instance, String methodName, Object... args) throws Exception {
return Whitebox.invokeMethod(instance, methodName, args);//调用私有方法
} }

测试用例

@PrepareForTest({PowerMockitoDemo.class, PropertiesUtil.class})//此处PowerMockitoDemo被测试类添加到@PrepareForTest注解中, 用于mock其静态、final修饰及私有方法;另外,PropertiesUtil工具类由于不通用,不适合抽取到基类BaseTest中, 可在子类mock
@SuppressStaticInitializationFor("com.test.util.PropertiesUtil")//用于阻止类中的静态代码块执行
public class PowerMockitoDemoTest extends BaseTest{ @org.powermock.core.classloader.annotations.Mock
private StudentDao studentDao; @org.powermock.core.classloader.annotations.Mock
private TeacherService teacherService; @org.powermock.core.classloader.annotations.Mock
@InjectMocks
private PowerMockitoDemo powerMockitoDemo; @Override
@Before
public void setUp() {
super.setUp();
PowerMockito.suppress(PowerMockito.constructor(PropertiesUtil.class));
PowerMockito.mockStatic(PropertiesUtil.class);//mock类中所有静态方法
} @Test
public void studyWhenCallSuccessfully() {
PowerMockito.doCallRealMethod().when(powerMockitoDemo).study();
//doSomething powerMockitoDemo.study();
} @Test
public void playWhenCallSuccessfully() {
Map<String, Object> project = new HashMap<String, Object>();
Person person = new Person();
int hours = 8; PowerMockito.doCallRealMethod().when(powerMockitoDemo, "play", Matchers.anyMapOf(String.class, Object.class), Matchers.any(Person.class), Matchers.anyInt());
//doSomething this.callPrivateMethod(powerMockitoDemo, "play", project, person, hours);
} @Test
public void updateStudentNameWhenCallSuccessfully() throws Exception {
String id = "9527"; PowerMockito.when(powerMockitoDemo, "updateStudentName", Matchers.anyString()).thenCallRealMethod();
//doSomething boolean actualResult = this.callPrivateMethod(powerMockitoDemo, "updateStudentName", id);
Assert.assertTrue(actualResult == true);
} @Test
public void getStudentByIdWhenCallSuccessfully() throws Exception {
List<String> usernames = new ArrayList<String>();
String rights = "万叶飞花流";
String orderid = "orderid";
String id = "id"; //调用真实方法
PowerMockito.when(powerMockitoDemo.grantRights(Matchers.anyListOf(String.class), Matchers.anyString(), Matchers.anyString(), Matchers.anyString())).thenCallRealMethod();
//当方法内重复调用同一个方法时, 可通过Matchers.eq()方法来指定实际入参来加以区分
PowerMockito.when(PropertiesUtil.get(Matchers.eq("key1"))).thenReturn("value1");
PowerMockito.when(PropertiesUtil.get(Matchers.eq("key2"))).thenReturn("value2");
//返回值类型为void,不做任何事情
PowerMockito.doNothing().when(studentDao).saveRecord(Matchers.anyListOf(String.class));
//类似需要调用数据库、redis、远程服务的,可直接模拟返回值,不做方法的真实调用
PowerMockito.when(studentDao.getStudentById(Matchers.anyString())).thenReturn(new Student());
//调用真实的私有方法
PowerMockito.when(powerMockitoDemo, "verifyParams", Matchers.anyListOf(String.class)).thenCallRealMethod();
//模拟私有方法返回值
PowerMockito.when(powerMockitoDemo, "verifyParams", Matchers.anyListOf(String.class)).thenReturn(true);
//模拟方法调用抛出异常。当被调用的方法头没有显式声明异常时, 则mock只支持unchecked exception,比如这里syncDB2Redis()方法签名没有声明任何异常,则thenThrow()模拟异常时只支持模拟运行时异常,使用非运行时异常将编译不通过
PowerMockito.when(teacherService.syncDB2Redis(Matchers.anyListOf(String.class), Matchers.anyString())).thenThrow(new RuntimeException("Failed to call remote service"));
PowerMockito.doNothing().when(powerMockitoDemo, "updateOperation", Matchers.anyString()); ServiceResult expectedResult = new ServiceResult();
ServiceResult actualResult = powerMockitoDemo.grantRights(usernames, rights, orderid, id);
//断言实际调用结果是否符合预期值
Assert.assertEquals(JSON.toJSONString(expectedResult), JSON.toJSONString(actualResult));
}
}

如上, 具体的阐释在代码注释中都已经标注, 抽取基类是为了提高代码可重用性。博主这里是为了演示, 所以代码看起来会有点臃肿, 在实际项目使用中, 可以通过静态引入 import static org.mockito.Mockito.when; 和 import static org.mockito.Matchers.*; 来简化代码, 提高可阅读性。

另外由于被测试类在测试方法中被mock掉, 且被@PrepareForTest注解标记时, JaCoCo工具统计测试覆盖率将忽略该测试类。可通过在基类BaseTest中添加 @Rule public ExpectedException thrown = ExpectedException.none(); , 并去掉 @RunWith(PowerMockRunner.class) 和 @SuppressStaticInitializationFor 来使统计测试覆盖率生效。但这里又有一个新问题产生, 基类修改之后, 发现测试用例无法进入debug调试, 因此建议先用修改前的基类来编写单元测试, 便于调试, 待测试用例完成后, 再修改基类令Jacoco统计覆盖率生效。

关于PowerMockito的实践, 博主目前在项目中的使用主要就涉及到了这些, 所以这次做了回标题党"XXX深入实践" ^_^, 后面如有接触新的相关知识点, 会陆续更新到本篇文章中。如有错误, 欢迎指正, 谢谢你^_^

参考资料

非web下的PowerMockito单元测试

powermockito单元测试之深入实践的更多相关文章

  1. [转载]单元测试之道(使用NUnit)

    首先来看下面几个场景你是否熟悉 1.你正在开发一个系统,你不断地编码-编译-调试-编码-编译-调试……终于,你负责的功能模块从上到下全部完成且编译通过!你长出一口气,怀着激动而又忐忑的心情点击界面上的 ...

  2. 单元测试之道(使用NUnit)

    首先来看下面几个场景你是否熟悉 1.你正在开发一个系统,你不断地编码-编译-调试-编码-编译-调试……终于,你负责的功能模块从上到下全部完成且编译通过!你长出一口气,怀着激动而 又忐忑的心情点击界面上 ...

  3. 补习系列(8)-springboot 单元测试之道

    目录 目标 一.About 单元测试 二.About Junit 三.SpringBoot-单元测试 项目依赖 测试样例 四.Mock测试 五.最后 目标 了解 单元测试的背景 了解如何 利用 spr ...

  4. ASP.NET Core搭建多层网站架构【3-xUnit单元测试之简单方法测试】

    2020/01/28, ASP.NET Core 3.1, VS2019, xUnit 2.4.0 摘要:基于ASP.NET Core 3.1 WebApi搭建后端多层网站架构[3-xUnit单元测试 ...

  5. iOS 单元测试之XCTest详解(一)

    iOS 单元测试之XCTest详解(一) http://blog.csdn.net/hello_hwc/article/details/46671053 原创blog,转载请注明出处 blog.csd ...

  6. 玩转单元测试之Testing Spring MVC Controllers

    玩转单元测试之 Testing Spring MVC Controllers 转载注明出处:http://www.cnblogs.com/wade-xu/p/4311657.html The Spri ...

  7. 玩转单元测试之WireMock -- Web服务模拟器

    玩转单元测试之WireMock -- Web服务模拟器 WireMock 是一个灵活的库用于 Web 服务测试,和其他测试工具不同的是,WireMock 创建一个实际的 HTTP服务器来运行你的 We ...

  8. 单元测试之NSNull 检测

    本文主要讲 单元测试之NSNull 检测,在现实开发中,我们最烦的往往就是服务端返回的数据中隐藏着NSNull的数据,一般我们的做法是通过[data isKindOfClass:[NSNull cla ...

  9. 使用VisualStudio进行单元测试之二

    借着工作忙的借口,偷了两天懒,今天继续单元测试之旅.前面说了如何进行一个最简单的单元测试,这次呢就跟大家一起来熟悉一下,在visual studio中如何进行数据驱动的单元测试. 开始之前先来明确一下 ...

随机推荐

  1. MediatR一个.net中简单好用的中介者模式实现方案

    MediatRGit地址:https://github.com/jbogard/MediatR 1.安装妞盖特包 一般来说只需要安装一个MediatR就行了,.net core程序需要再安装一个Med ...

  2. 第四届蓝桥杯省赛 (JavaB组)

    第二题:马虎的算式 小明是个急性子,上小学的时候经常把老师写在黑板上的题目抄错了. 有一次,老师出的题目是:36 x 495 = ? 他却给抄成了:396 x 45 = ? 但结果却很戏剧性,他的答案 ...

  3. Ubuntu18.04安装postgresql-10

    Ubuntu18安装postgresql-10 最近切换Ubuntu作为办公系统,所有软件安装都要重来一遍. 官方文档: https://www.postgresql.org/download/lin ...

  4. python初识(3)

    bool 字符串 for循环 bool 数字非零全都是True 字符串非空全都是True 字符串 索引 从0开始 0 切片选取 [x:y] 左闭右开区间 [x:y:z] 选取x到y之间 每隔z选取一次 ...

  5. NEUOJ 1702:撩妹全靠魅力值(CDQ分治三维偏序)

    http://acm.neu.edu.cn/hustoj/problem.php?id=1702 思路:三维偏序模板题,用CDQ分治+树状数组或者树套树.对于三元组(x,y,z),先对x进行排序,然后 ...

  6. 超哥的 LINUX 入门大纲

    前言 “Linux?听说是一个操作系统,好用吗?” “我也不知道呀,和windows有什么区别?我能在Linux上玩LOL吗” “别提了,我用过Linux,就是黑乎乎一个屏幕,鼠标也不能用,不停地的敲 ...

  7. python 实现一个计算器功能

    #s = '1 - 2 * ( (60-30 +(-40/5) * (9-2*5/3 + 7 /3*99/4*2998 +10 * 568/14 )) - (-4*3)/ (16-3*2) )' #第 ...

  8. 把大象装进冰箱:HTTP传输大文件的方法

    上次我们谈到了HTTP报文里的div,知道了HTTP可以传输很多种类的数据,不仅是文本,也能传输图片,音频和视频.   早期互联网上传输的基本上都是只有几k大小的文本和小图片,现在的情况则大有不同.网 ...

  9. 用SpringBoot+MySql+JPA实现对数据库的增删改查和分页

    使用SpringBoot+Mysql+JPA实现对数据库的增删改查和分页      JPA是Java Persistence API的简称,中文名Java持久层API,是JDK 5.0注解或XML描述 ...

  10. 嵊州D3T2 福尔贝斯太太的快乐夏日 summer

    宗教,或是无节制的自由主义,是致人腐化的毒剂. 现在,一个人要经历 n 个事件,编号为 1 ∼ n. 经历 x 号事件,他的危险值就会增加 x. 一开始他的危险值是 0. 当一个人的危险值大于 0 且 ...