Mockito和PowerMock
编写单元测试时,最常用就是利用mockito、PowerMock进行mock对象。
可以帮我们mock private方法、static方法、静态类等。
一个常见的单元测试如下:这里也mock了static方法。
被测试代码:
@Component
public class SelfService {
@Autowired
private SelfCommand selfCommand;
@Autowired
private SelfNotify selfNotify;
public boolean checkParam(Integer param) {
String message = MessageConvert.convert(param);
selfNotify.notify(message);
return check(param);
}
private boolean check(int key) {
return selfCommand.check(key);
}
}
至于使用到的两个类,尚未实现。我们可以mock他们。
@Component
public class SelfNotify {
public void notify(String content) {
// TODO
}
}
@Component
public class SelfCommand {
public boolean check(int key) {
return false;
}
}
测试代码如下:
@RunWith(PowerMockRunner.class)
@PowerMockIgnore({"javax.management.*", "javax.security.*"}) //忽略一些mock异常
@PrepareForTest({MessageConvert.class})// 因为mock 静态方法,要prepare
public class SelfServiceTest {
// 把被模拟对象注入.因为PowerMock会自动生成对象,因此这里不能SelfService 不能是接口
@InjectMocks
private SelfService selfService;
@Mock
private SelfCommand selfCommand;
// mock 被测试类依赖的属性对象
@Mock
private SelfNotify selfNotify;
@Before
public void setup() {
// 因为是void 方法只能用 doNothing()
// 注意这里的when括号里面只有对象,方法调用写在括号外边
PowerMockito.doNothing().when(selfNotify).notify(ArgumentMatchers.anyString());
// mock 静态方法
PowerMockito.mockStatic(MessageConvert.class);
}
@Test
public void should_get_true_when_pass_check() {
when(selfCommand.check(ArgumentMatchers.anyInt())).thenReturn(true);
when(MessageConvert.convert(ArgumentMatchers.anyInt())).thenReturn("mockStatic");
boolean checkResult = selfService.checkParam(2);
assertThat(checkResult).isTrue();
// 验证被Mock的对象的方法是不是真的被调用了,verify 第二个参数默认为被调用了一次
Mockito.verify(selfCommand).check(ArgumentMatchers.eq(2));
Mockito.verify(selfNotify).notify(ArgumentMatchers.eq("mockStatic"));
}
// 测试私有方法
@Test
public void should_can_test_private_method() throws Exception {
when(selfCommand.check(ArgumentMatchers.anyInt())).thenReturn(true);
boolean checkResult = Whitebox.invokeMethod(selfService, "check", 1);
assertThat(checkResult).isTrue();
}
}
这里测试了私有代码,也mock了静态方法。确实很好用,不过使用的时候也碰到写问题。
问题一:无法直接mock私有方法
类中 public方法A调用 private方法B。即 A =>B 时,无法直接mock整个方法 B。需要写一大堆mock控制B的返回。
// 无法mock private 方法
@Test
public void should_mock_private_for_test_class() throws Exception {
// 想mock被测试类的某个private 方法,不起作用
PowerMockito.when(selfService, "check", ArgumentMatchers.any()).thenReturn(true);
boolean checkResult = selfService.checkParam(2);
assertThat(checkResult).isTrue();
}
此处我更想做的是直接mock 方法B的返回。
问题二:注意基本参数类型,防止NPE
// 注意基本类型. 这里会失败
@Test
public void should_get_true_when_pass_check_1() {
// 这里因为是基本类型,用any()时可能会 NullPointerException
when(selfCommand.check(ArgumentMatchers.anyInt())).thenReturn(true);
boolean checkResult = selfService.checkParam(null);
assertThat(checkResult).isTrue();
}
这里 SelfCommand.check(int key)的参数是基本类型,会导致对 null拆箱,报NPE。不过这个是java本身的问题,与Mockito无关。
问题三:测试类的继承,避免属性重复
我们通常会把一些通用的方法放在基类里面,这样在写每个测试类时,可以少做一些东西。
@RunWith(PowerMockRunner.class)
@PowerMockIgnore({"javax.management.*", "javax.security.*"}) //忽略一些mock异常
public class BaseTest {
@Mock
protected CacheUtil cacheUtil;
@Before
public void init() {
when(cacheUtil.get(ArgumentMatchers.any())).thenReturn("Unit Test");
}
}
具体测试类:
public class SelfServiceTest extends BaseTest {
// 把被模拟对象注入.因为PowerMock会自动生成对象,因此这里不能SelfService 不能是接口
@InjectMocks
private SelfService selfService;
@Mock
private SelfCommand selfCommand;
// mock 被测试类依赖的属性对象
// 这里因为父类也 mock 了 该类,会导致使用时空指针
// @Mock
// private CacheUtil cacheUtil;
@Test
public void should_get_cache_value() {
when(cacheUtil.get(ArgumentMatchers.any())).thenReturn("Test");
String value = selfService.getCacheValue("key");
assertThat(value).isEqualToIgnoringCase("Test");
}
}
这里测试类中如果也加了与父类相同的属性时,会导致NPE。
问题四:先后两次使用相同参数调用同一个方法
该问题可能因为代码习惯不好。Mockito可以让我们根据不同参数mock不同的返回值:
@Test
public void should_get_different_for_differ_key() {
when(cacheUtil.get(ArgumentMatchers.eq("key1"))).thenReturn("Unit");
when(cacheUtil.get(ArgumentMatchers.eq("key2"))).thenReturn("Test");
String value1 = selfService.getCacheValue("key1");
String value2 = selfService.getCacheValue("key2");
assertThat(value1 + value2).isEqualToIgnoringCase("UnitTest");
}
这里根据参数不同mock不同的返回。但是,两次参数一样呢?
@Component
public class SelfService {
@Autowired
private SelfCommand selfCommand;
@Autowired
private SelfNotify selfNotify;
@Autowired
private CacheUtil cacheUtil;
// ...其他方法
public String calculateValue(String key) {
String cache = cacheUtil.get(key);// 1
if (StringUtils.isNotEmpty(cache)) {
return cache;
}
String value = selfCommand.get(key);
cacheUtil.set(key, value);
return cacheUtil.get(key);// 2
}
}
这里代码可能写的不好。对于1和2处参数一样,不过缺需要不同的返回值:需要1处返回空字符串,2处返回真正的数据。
尝试了下,可以这样写
when(myService.queryById(ArgumentMatchers.anyLong())).thenReturn(null).thenThrow(Exception.class);
第一调用返回null,第二次抛出异常。
问题五:修改入参对象属性
如下,CoefficientFactor对象作为参数传递给SelfCommand的calculate()方法,在该方法里面对其进行了修改。此时mock无法做到
@Component
public class SelfService {
@Autowired
private SelfCommand selfCommand;
// ...省略
public Long calculate(Long userId, CoefficientFactor factor) {
// 当selfCommand.calculate 里面有对 factor进行set 设置值时。此时mock无法模拟
Long result = selfCommand.calculate(userId, factor);
// ... 此处代码可能使用factor的属性
return result * 4;
}
}
问题六:链式调用
看下这个例子,利用框架进行了流式校验,代码很简洁,但是mock时有点难了。
@Component
public class CombineValidator {
public MyValidatorResult validate(MyDto mydto, String orderSn) {
ComplexResult commonResult = FluentValidator.checkAll().failOver()
.putAttribute2Context("orderSn", orderSn)
.on(mydto, commonValidator)
.doValidate(commonValidatorCallback).result(toComplex());
ComplexResult ret = FluentValidator.checkAll().failOver()
.putAttribute2Context("orderSn", orderSn)
.on(orderSn, orderValidator)
.on(mydto.getUid(), riskDelayValidator)
.on(mydto.getOperationType, operatorValidator)
.doValidate().result(toComplex());
if (commonResult.isSuccess()) {
// xxx
}
if (ret.isSuccess()) {
// xxx
}
}
}
此时因为链式调用并导致了一些封装,对result方法mock起来有些麻烦。