学习一门计算机语言,我以为除了学习它的语法外,最重要的就是要学习怎么在这个语言环境下进行单元测试,由于单元测试能帮你提前发现错误;同时给你的程序加一道防御网,防止你的修改破坏了原有的功能;单元测试还能指引你写出更好的代码,毕竟不能被测试的代码必定不是好代码;除此以外,它还能增长你的自信,能勇敢的说出「个人程序没有bug」。java
每一个语言都有其经常使用的单元测试框架,本文主要介绍在 Java 中,咱们如何使用 PowerMock,来解决咱们在写单元测试时遇到的问题,从 Mock 这个词能够看出,这类问题主要是解依赖问题。git
在写单元测试时,为了让测试工做更简单、减小外部的不肯定性,咱们通常都会把被测类和其余依赖类进行隔离,否则你的类依赖得越多,你须要作的准备工做就越复杂,尤为是当它依赖网络或外部数据库时,会给测试带来极大的不肯定性,而咱们的单测必定要知足快速、可重复执行的要求,因此隔离或解依赖是必不可少的步骤。github
而 Java 中的 PowerMock 库是一个很是强大的解依赖库,下面谈到的 3 个特性,能够帮你解决绝大多数问题:spring
假设你有两个类,MyService
和 MyDao
,MyService
依赖于 MyDao
,且它们的定义以下数据库
// MyDao.java
@Mapper
public interface MyDao {
/** * 根据用户 id 查看他最近一次操做的时间 */
Date getLastOperationTime(long userId);
}
// MyService.java
@Service
public class MyService {
@Autowired
private MyDao myDao;
public boolean operate(long userId, String operation) {
Date lastTime = myDao.getLastOperationTime(userId);
// ...
}
}
复制代码
这个服务提供一个 operate
接口,用户在调用该接口时,会被限制一个操做频次,因此系统会记录每一个用户上次操做的时间,经过 MyDao.getLastOperationTime(long userId)
接口获取,如今咱们要对 MyService
类的 operate
作单元测试,该怎么作?springboot
你可能会想到使用 SpringBoot,它能自动帮咱们初始化 myDao
对象,但这样作却存在一些问题:网络
因为以上缘由,咱们通常在作单元测试时,不启动 SpringBoot 上下文,而是采用 PowerMock 帮咱们注入依赖,对于上面的 case,咱们的测试用例能够这样写:app
// MyServiceTest.java
@RunWith(PowerMockRunner.class)
@PrepareForTest({MyService.class, MyDao.class})
public class MyServiceTest {
@Test
public void testOperate() throws IllegalAccessException {
// 构造一个和当前调用时间永远只差 4 秒的返回值
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND, -4);
Date retTime = calendar.getTime();
// spy 是对象的“部分 mock”
MyService myService = PowerMockito.spy(new MyService());
MyDao md = PowerMockito.mock(MyDao.class);
PowerMockito
.when(md.getLastOperationTime(Mockito.any(long.class)))
.thenReturn(retTime);
// 替换 myDao 成员
MemberModifier.field(MyService.class, "myDao").set(myService, md);
// 假设最小操做的间隔是 5 秒,不然返回 false
Assert.assertFalse(myService.operate(1, "test operation"));
}
}
复制代码
从上面代码中,咱们首先构造了一个返回时间 retTime
,模拟操做间隔的时间为 4 秒,保证了每次运行测试时该条件不会变化;而后咱们用 spy
构造一个待测试的 MyService
对象,spy
和 mock
的区别是,spy
只会部分模拟对象,即这里只修改掉 myService.myDao
成员,其余的保持不变。框架
而后咱们定义了被 mock 的对象 MyDao md
的调用行为,当 md.getLastOperationTime
函数被调用时,返回咱们构造的时间 retTime
,此时测试环境就设置完毕了,这样作以后,你就能够很容易的测试 operate
函数了。ide
上文所说的使用 PowerMock 进行依赖注入,能够覆盖测试中绝大多数的解依赖场景,而另外一种常见的依赖是 static 函数,例如咱们本身写的一些 CommonUtil
工具类中的函数。
仍是使用上面的例子,假设咱们要计算当前时间和用户上一次操做时间之间的间隔,并使用 public static long getTimeInterval(Date lastTime)
实现该功能,以下:
// CommonUtil.java
class CommonUtil {
public static long getTimeInterval(Date lastTime) {
long duration = Duration.between(lastTime.toInstant(),
new Date().toInstant()).getSeconds();
return duration;
}
}
复制代码
咱们的 operator
函数修改以下
// MyService.java
// ...
public boolean operate(long userId, String operation) {
Date lastTime = myDao.getLastOperationTime(userId);
long duration = CommonUtil.getTimeInterval(lastTime);
if (duration >= 5) {
System.out.println("user: " + userId + " " + operation);
return true;
} else {
return false;
}
}
// ...
复制代码
这里先从 myDao
获取上次操做的时间,再调用 CommonUtil.getTimeInterval
计算操做间隔,若是小于 5 秒,就返回 false
,不然执行操做,并返回 true
。那么个人问题是,如何解掉这里 static 函数的依赖呢?咱们直接看测试代码吧
// MyServiceTest.java
@PrepareForTest({MyService.class, MyDao.class, CommonUtil.class})
public class MyServiceTest {
// ...
@Test
public void testOperateWithStatic() throws IllegalAccessException {
// ...
PowerMockito.spy(CommonUtil.class);
PowerMockito.doReturn(5L).when(CommonUtil.class);
CommonUtil.getTimeInterval(Mockito.anyObject());
// ...
}
}
复制代码
首先在注解 @PrepareForTest
中增长 CommonUtil.class
,依然使用 spy
对类 CommonUtil
进行 mock,若是不这么作,这个类中全部静态函数的行为都会发生变化,这会给你的测试带来麻烦。spy
下面的两行代码你应该放在一块儿解读,意为当调用 CommonUtil.getTimeInterval
时,返回 5;这种写法比较奇怪,但倒是 PowerMock 要求的。至此,你已经掌握了 mock static 函数的技巧。
有些函数会经过修改参数所引用的对象做为输出,例以下面的这个场景,假设咱们的 operation 是一个长时间执行的任务,咱们须要不断轮训该任务的状态,更新到内存,并对外提供查询接口,以下代码:
// MyTask.java
// ...
public boolean run() throws InterruptedException {
while (true) {
updateStatus(operation);
if (operation.getStatus().equals("success")) {
return true;
} else {
Thread.sleep(1000);
}
}
}
public void updateStatus(Operation operation) {
String status = myDao.getStatus(operation.getOperationId());
operation.setStatus(status);
}
// ...
复制代码
上面的代码中,run()
是一个轮询任务,它会不断更新 operation 的状态,并在状态达到 "success"
时中止,能够看到,updateStatus
就是咱们所说的函数,虽然它没有返回值,但它会修改参数所引用的对象,因此这种参数也被称做输出参数。
如今咱们要测试 run()
函数的行为,看它是否会在 "success"
状态下退出,那么咱们就须要 mock updateStatus
函数,该怎么作?下面是它的测试代码:
@Test
public void testUpdateStatus() throws InterruptedException {
// 初始化被测对象
MyTask myTask = PowerMockito.spy(new MyTask());
myTask.setOperation(new MyTask.Operation());
// 使用 doAnswer 来 mock updateStatus 函数的行为
PowerMockito.doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
MyTask.Operation operation = (MyTask.Operation)args[0];
operation.setStatus("success");
return null;
}
}).when(myTask).updateStatus(Mockito.any(MyTask.Operation.class));
Assert.assertEquals(true, myTask.run());
}
复制代码
上面的代码中,咱们使用 doAnswer
来 mock updateStatus
的行为,至关于使用 answer
函数来替换原来的 updateStatus
函数,在这里,咱们将 operation
的状态设置为了 "success"
,以期待 myTask.run()
函数返回 true
。因而,咱们又学会了如何 mock 具备输出参数的函数了。
以上代码只为了说明应用场景,并不是生产环境级别的代码,且均经过测试,为方便后续学习,你能够在这里下载:github.com/jieniu/arti…
参考: