PowerMockito实战——如何使用PowerMockito写好UT
引言
使用mock框架进行单元测试在很长一段时间内都被认为是一种有效实践,特别是Mockito框架在最近几年占据了这个市场的主导地位。它基于测试分层的思想,通过对于外部依赖的打桩,让开发者从组件依赖中抽身出来,只聚焦于待测类的逻辑与结果。 PowerMockito是PowerMock的一个扩展API,用于支持Mockito。它提供了以简单的方式使用Java反射API的功能,以克服Mockito缺乏mock final、static或private方法能力的不足。
本文将介绍PowerMockito API,并结合实战展示它在单元测试中的应用。
准备
引入maven依赖
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>1.6.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
在测试类头声明注解
@RunWith(PowerMockRunner.class)
使用@InjectMocks和@Mock将待测类及其依赖的mock组件完成创建并注入,示例如下:
待测类:
测试类:
打桩
打桩指在单元测试中使用“桩”(mock)来代替真实对象,为真实对象的方法调用设定好开发者所期望的返回并将整个交互过程交给桩来模拟进行,以便帮助开发人员在测试中忽略外部调用。
方法调用通常有两种交互,返回和异常,PowerMockito中对应的API如下:
PowerMockito.when(...).thenReturn(...)或Powermockito.doReturn(...).when(...)
PowerMockito.when(...).thenThrow(...)或Powermockito.doThrow(...).when(...)
对于实例方法、静态方法、构造方法在UT中的打桩示例如下:
实例方法:
sourceCode:
ProductInfo productInfo = productDao.queryByAppId(appId);
testCode:
ProductInfo productInfo = new ProductInfo();
productInfo.setAppVersion("version");
PowerMockito.when(productDao.queryByAppId("appId")).thenReturn(productInfo);
当我们期望被测方法中依赖的数据库交互对象productDao的查询方法queryByAppId返回一个appVersion属性的值为“version”的ProductInfo对象时,我们可以以上述方式为实例方法调用打桩。此时运行测试,程序执行到该实例方法调用时将返回我们事先设定好的ProductInfo对象。
静态方法:
sourceCode:
String info = XXXX1.getByAppid(appId, true);
testCode:
PowerMockito.mockStatic(XXXX1.class);
PowerMockito.when(XXXX1.getByAppid("appId",true)).thenReturn("info");
当我们期望被测方法中依赖的静态调用XXXX1.getByAppid返回一个值为“info”的String对象时,我们可以以上述方式为静态方法调用打桩。此时运行测试,程序执行到该静态方法调用时将返回我们事先设定好的String对象。
注意,在给静态方法的调用进行打桩或验证时需要给测试类头上加上@PrepareForTest注解,并将该静态方法的所在类传入注解,多个类以数组形式如图:
构造方法:
sourceCode:
XXXX2 obj = new XXXX2();
try {
isSucceed = obj.method(appId);
}
testCode:
XXXX2 mock = PowerMockito.mock(XXXX2.class);
PowerMockito.whenNew(XXXX2.class).withNoArguments().thenReturn(mock);
当我们期望被测方法中调用构造方法创建出一个XXXX2类型的对象时,我们可以以上述方式为构造方法调用打桩。此时运行测试,程序执行到该构造方法调用时将返回我们事先设定好的XXXX2类型的桩对象(以便后续对该对象的方法调用继续打桩,如本例中还需进一步对obj.method这一方法调用打桩)。
注意,在给构造方法的调用进行打桩或验证时需要将构造方法调用所在类传入@PrepareForTest注解
验证
在单元测试中有时我们并不关心一些方法的返回,甚至这些方法可能并没有返回值(如Dao层对象的数据插入、更新操作),而我们可以凭借桩对象的行为(方法调用)来确保待测方法逻辑的正确性。
对于实例方法、静态方法、构造方法在UT中的验证示例如下:
实例方法:
sourceCode:
xxxxxxDao.updateStatusByAppId(appId, "3");
testCode:
PowerMockito.verifyPrivate(xxxxxxDao).invoke("updateStatusByAppId", "appId", "3");
静态方法:
sourceCode:
XXXX2.updateState(appid, String.valueOf(state));
testCode:
PowerMockito.verifyStatic(XXXX2.class, Mockito.times(1));
XXXX2.updateState("appId", "state");
verify方法默认验证方法调用1次,故times(1)可忽略,如需验证多次调用可修改参数匹配实际调用次数。
注意:静态方法的verify后需要再次手动调用以通知PowerMockito要验证的是该类的哪一个静态方法
构造方法:
sourceCode
sourceCode:
XXXX1 obj = new XXXX1();
testCode:
XXXX1 mockedObj = PowerMockito.mock(XXXX1.class);
PowerMockito.whenNew(XXXX1.class).withNoArguments().thenReturn(mockedObj);
PowerMockito.verifyNew(XXXX1.class).withNoArguments();
参数匹配器
参数匹配器(org.mockito.ArgumentMatchers)通常应用在打桩或验证API中,使得打桩和验证变得更加灵活、简洁。
常用参数匹配器有any(),anyBoolean(),anyInt(),anyString(),eq()等,详见ArgumentMatchers (Mockito 2.2.7 API)
以上面的示例进行说明:
不使用参数匹配器的打桩:
PowerMockito.when(productDao.queryByAppId("appId")).thenReturn(productInfo);
当方法调用参数为“appId”时才会返回我们预先设定的productInfo对象,当参数改变为其他则不生效,无法返回此对象。
使用参数匹配器的打桩:
PowerMockito.when(productDao.queryByAppId(anyString())).thenReturn(productInfo);
当方法调用参数为只要是String类型的对象都会返回我们预先设定的productInfo对象。
不使用参数匹配器的验证:
PowerMockito.verifyPrivate(xxxxxxDao).invoke("updateStatusByAppId", "1", "3");
当方法调用参数为“1”和“3”时才会命中我们的验证,当参数改变为其他则不生效,verify方法抛出异常:
Argument(s) are differemt! Wanted:
xxxxxxDao.updateStatusByAppId("1","1");
Actual invocations have different arguments:
xxxxxxDao.updateStatusByAppId("1","3");
使用参数匹配器的打桩:
PowerMockito.verifyPrivate(xxxxxxDao).invoke("updateStatusByAppId", anyString(), anyString());
当方法调用参数为只要都是String类型的对象都会命中我们的验证。
注意:当使用参数匹配器时,要确保所有参数都使用了参数匹配器,不允许一部分参数使用参数匹配器一部分不使用。比如下面这行代码就是个典型的错误示例:
PowerMockito.verifyPrivate(xxxxxxDao).invoke("updateStatusByAppId", anyString(), "3");
这种混用情况将抛出异常:
This exception may occur if matchers are combined with raw values:
//incorrect:
someMethod(anyObject(),"raw String");
When using matchers, all arguments have to be provided by matchers.
For example:
//correct:
someMethod(anyObject(),eq("String by matcher"));
正确写法已经在错误信息中提示出:对于这种部分交给参数匹配器,一部分参数值固定的情况需要使用eq()将固定的参数转换为一个参数匹配器,即:
PowerMockito.verifyPrivate(xxxxxxDao).invoke("updateStatusByAppId", anyString(), eq("3"));
参数捕获器
参数捕获器(ArgumentCaptor)允许在verify的时候获取方法调用的参数,这使我们在测试过程中能够更细粒度的对方法调用行为进行测试。特别是在方法调用参数中含有复杂对象作为参数时,此时如果在verify中只用参数匹配器则丢失了参数对象内部的属性细节,而去构造一个完整的对象又会增加测试工作量降低效率,这时就能体现出参数捕获器的强大,既能细粒度的关注到参数对象内部的细节又免于繁琐的构造工作。
参数捕获器需使用@Captor注解来声明一个包含期望捕获的参数类型的捕获器,在函数调用的verify中使用captor.capture()对此参数进行捕获,并对其内部细节进行断言。示例如下:
断言
一个完整的单元测试,一般都有断言的参与,对于有返回值的方法,我们可以对其返回值的某些字段进行断言,以验证待测方法的逻辑符合预期;对于没有返回值的方法则可以使用前文提及的verify来验证待测方法中的行为符合预期;对于有异常抛出的情况,也可以对异常信息进行断言,以验证待测方法中异常处理的逻辑符合预期。
常用的断言API(org.junit.Assert)如下:
assertEquals(),assertNotEquals(),assertNull(),assertTrue(),asserThat()等,详见Assert (JUnit API)
返回值断言示例:
XXXXXInfo info = xxxxxxService.findById("1");
assertEquals("0", info.getCurrentStatus());
assertEquals("name", info.getName());
assertEquals("1", info.getLastStatus());
异常信息断言在低版本Junit4(before4.13)中需要借助@Rule注解来完成,高版本Junit4及Junit5可使用assertThrows(详见Assert (JUnit API))来对异常类中的属性,如errorCode,errorMessage等进行断言。
异常信息断言示例:
sourceCode:
if (StringUtils.isEmpty(appId)) {
throw new XXException(ERROR_MSG, ERROR_CODE);
}
testCode;
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void shouldThrowExceptionWithErrorCodeEqualsERROR_CODE() throws XXException {
thrown.expect(XXException.class);
thrown.expect(hasProperty("errorCode", is(ERROR_CODE)));
xxxxService.method("");
}
注意:此种实现方式的异常断言需要声明在待测方法调用之前