Java 中的 UnitTest 和 PowerMock

UnitTest 和 PowerMock

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

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

在写单元测试时,为了让测试工做更简单、减小外部的不肯定性,咱们通常都会把被测类和其余依赖类进行隔离,否则你的类依赖得越多,你须要作的准备工做就越复杂,尤为是当它依赖网络或外部数据库时,会给测试带来极大的不肯定性,而咱们的单测必定要知足快速、可重复执行的要求,因此隔离或解依赖是必不可少的步骤。github

而 Java 中的 PowerMock 库是一个很是强大的解依赖库,下面谈到的 3 个特性,能够帮你解决绝大多数问题:spring

  1. 经过 PowerMock 注入依赖对象
  2. 利用 PowerMock 来 mock static 函数
  3. 输出参数(output parameter)怎么 mock

经过 PowerMock 注入依赖对象

假设你有两个类,MyServiceMyDaoMyService 依赖于 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 对象,但这样作却存在一些问题:网络

  1. SpringBoot 的启动速度很慢,这会延长单元测试的时间
  2. 由于时间是一个不断变化的量,也许这一次你构造的时间知足测试条件,但下一次运行测试时,可能就不知足了。

因为以上缘由,咱们通常在作单元测试时,不启动 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 对象,spymock 的区别是,spy 只会部分模拟对象,即这里只修改掉 myService.myDao 成员,其余的保持不变。框架

而后咱们定义了被 mock 的对象 MyDao md 的调用行为,当 md.getLastOperationTime 函数被调用时,返回咱们构造的时间 retTime,此时测试环境就设置完毕了,这样作以后,你就能够很容易的测试 operate 函数了。ide

利用 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<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…

参考:

相关文章
相关标签/搜索