java unit test moke_详解Java 中的UnitTest 和 PowerMock

学习一门计算机语言,我觉得除了学习它的语法外,最重要的就是要学习怎么在这个语言环境下进行单元测试,因为单元测试能帮你提早发现错误;同时给你的程序加一道防护网,防止你的修改破坏了原有的功能;单元测试还能指引你写出更好的代码,毕竟不能被测试的代码一定不是好代码;除此之外,它还能增加你的自信,能勇敢的说出「我的程序没有bug」。

每个语言都有其常用的单元测试框架,本文主要介绍在 Java 中,我们如何使用 PowerMock,来解决我们在写单元测试时遇到的问题,从 Mock 这个词可以看出,这类问题主要是解依赖问题。

在写单元测试时,为了让测试工作更简单、减少外部的不确定性,我们一般都会把被测类和其他依赖类进行隔离,不然你的类依赖得越多,你需要做的准备工作就越复杂,尤其是当它依赖网络或外部数据库时,会给测试带来极大的不确定性,而我们的单测一定要满足快速、可重复执行的要求,所以隔离或解依赖是必不可少的步骤。

而 Java 中的 PowerMock 库是一个非常强大的解依赖库,下面谈到的 3 个特性,可以帮你解决绝大多数问题:

1 通过 PowerMock 注入依赖对象

2 利用 PowerMock 来 mock static 函数

3 输出参数(output parameter)怎么 mock

通过 PowerMock 注入依赖对象

假设你有两个类,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,它能自动帮我们初始化 myDao 对象,但这样做却存在一些问题:

1 SpringBoot 的启动速度很慢,这会延长单元测试的时间

2 因为时间是一个不断变化的量,也许这一次你构造的时间满足测试条件,但下一次运行测试时,可能就不满足了。

由于以上原因,我们一般在做单元测试时,不启动 SpringBoot 上下文,而是采用 PowerMock 帮我们注入依赖,对于上面的 case,我们的测试用例可以这样写:

// 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 函数了。

利用 PowerMock 来 mock static 函数

上文所说的使用 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 函数的技巧。

输出参数(output parameter)怎么 mock

有些函数会通过修改参数所引用的对象作为输出,例如下面的这个场景,假设我们的 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() {

@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 具有输出参数的函数了。

以上就是详解Java 中的UnitTest 和 PowerMock的详细内容,更多关于Java UnitTest 和 PowerMock的资料请关注脚本之家其它相关文章!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值