文章目录
导航参见:
Mockito+PowerMock单元测试常见问题与解决方案
一、背景
我亦无他,唯手熟尔。写单元测试就是这种感觉,当你前面用Mockito+PowerMock框架写单测不多,对其原理不太了解的时候,程序运行总是报各种问题,然后就陷入到了一杯茶一包烟一个bug找一天的困境了…本篇文章主要就是根据大家写单测时可能会踩的坑总结而出,为了帮助对单元测试不太熟悉的朋友快速解决问题。
二、Mockito+PowerMock单元测试常见问题与解决方案
常见问题1:单测覆盖率问题
问题:
单测执行成功,debug打断点也能执行到被测方法里,但是覆盖率报告显示没有覆盖。
解决方式:
单测所在的类需要放在test包下和被测类同目录层级下,如被测类DtsAccountService放在src/main/java 目录com.qiguliuxing.dts.db.service下,那么测试类DtsAccountServiceTest需要相应放在src/test/java 目录com.qiguliuxing.dts.db.service下.
常见问题2:静态代码块初始化问题
问题:
当mock有静态方法的类时,由于其静态代码块或静态变量加载了一些第三方资源导致其不能被正常mock,从而抛出java.lang.ExceptionInInitializerError
解决方式:
利用@SuppressStaticInitializationFor注解加上要抑制的包名和类名,如下例所示:
//用于阻止类中的静态代码块执行
@SuppressStaticInitializationFor({"com.test.util.RedisUtils","com.test.util.HttpUtils"})
常见问题3:类型转换问题
问题:
如果运行时报:java.lang.ClassCastException: com.sun.crypto.provider.DESedeCipher cannot be cast to javax.crypto.CipherSpi…
即出现类型转换的问题。
解决方式:
由于PowerMock使用自定义的类加载器来加载类,可能导致系统类加载器认为有类型转化问题。解决方式如下:
@PowerMockIgonre({"javax.crypto.*"})
// 如果待测试类中使用到了XML解析相关的包和类,那么测试类前同样需要增加"org.xml.*", "javax.xml.*"
@PowerMockIgonre({"javax.crypto.*","org.xml.*","javax.xml.*"})
常见问题4:链式调用报NPE问题
问题:
我们在创建mock对象时,有的方法我们没有进行stubbing,所以调用时会返回Null,这样在进行操作是会抛出NullPointerException的。如下被测方法里有使用这种链式调用,如果我们只是进行简单的Mock,必然会报npe问题。
// 被测方法里局部代码
User user = new User("HuGe", new House());
String address = user.getHouse().getAddress();
// 错误mock方式
User user = Mockito.mock(User.class);
解决方式:
面对这种情况,我们有如下几种处理方式
// 方式一:利用Answers.RETURNS_DEEP_STUBS,RETURNS_DEEP_STUBS参数程序会自动进行mock所需的对象
User user = Mockito.mock(User.class, Answers.RETURNS_DEEP_STUBS);
// 方式二:手动分步骤mock
User user = Mockito.mock(User.class);
House house = Mockito.mock(House.class);
Mockito.when(user.getHouse()).thenReturn(house);
// 方式三:不用mock对象,用真实对象搭建其依赖关系
User user = new User();
House house = new House();
user.setHouse(house);
常见问题5:类未准备问题
问题:
如果运行时报:org.powermock.api.mockito.ClassNotPreparedException: The class XXX not prepared for test.
解决方式:
将XXX.class添加到@PrepareForTest中。这里值得说的是我们脑海里要形成思维定式,在mock涉及到final类、静态方法所在类、私有方法所在类时,都需要将其写到@PrepareForTest中。
常见问题6:对非mock对象执行打桩动作问题
问题:
如果运行时报:org.mockito.exceptions.misusing.MissingMethodInvocationException: when() requires an argument which has to be ‘a method call on a mock’.
解决方式:
when()方法中的参数要求是一个mock对象的方法调用。报该错误的原因是调用方法的对象不是mock出来的对象。如果是调用静态方法,则使用PowerMockito.mockStatic(类名.class)
常见问题7:参数不匹配问题1
问题:
如果在打桩时mock方法的参数既包含真实值,又包含模拟值,遇到如图中问题。
解决方式:
调整对mock方法的入参,要么同时用真实值,要么同时用模拟值,如Mockito.anyInt()、Mockito.anyLong()…
常见问题8:参数不匹配问题2
问题:
打桩时,若mock出来的方法入参(Mockito.anyLong()等方法)无法打桩成功时,例如如下写法:
Mockito.when(xxxService.methodA(Mockito.anyLong(), Mockito.anyBoolean())).thenReturn(new ArrayList());
解决方式:
将所有的入参替换为调用时的传入真实值即可解决问题,如下:
Mockito.when(xxxService.methodA(111L, false)).thenReturn(new ArrayList());
常见问题9:无返回值方法断言问题
问题:
对于无返回值方法且方法入参没有改变,那么这种情况下怎么mock以及断言呢?
解决方式:
对于这种类型的单元测试编写需要用到Mockito.spy()方法或者@Spy注解
// 被测类及被测方法
public class WxTemplateSender {
private final Log logger = LogFactory.getLog(WxTemplateSender.class);
@Autowired
private WxMaService wxMaService;
@Autowired
private DtsUserFormIdService formIdService;
/**
* 发送微信消息(模板消息),不带跳转
*
* @param touser
* 用户 OpenID
* @param templatId
* 模板消息ID
* @param parms
* 详细内容
*/
public void sendWechatMsg(String touser, String templatId, String[] parms) {
sendMsg(touser, templatId, parms, "", "", "");
}
private void sendMsg(String touser, String templatId, String[] parms, String page, String color,
String emphasisKeyword) {
DtsUserFormid userFormid = formIdService.queryByOpenId(touser);
if (userFormid == null)
return;
WxMaTemplateMessage msg = new WxMaTemplateMessage();
msg.setTemplateId(templatId);
msg.setToUser(touser);
msg.setFormId(userFormid.getFormid());
msg.setPage(page);
msg.setColor(color);
msg.setEmphasisKeyword(emphasisKeyword);
msg.setData(createMsgData(parms));
try {
wxMaService.getMsgService().sendTemplateMsg(msg);
if (formIdService.updateUserFormId(userFormid) == 0) {
logger.warn("更新数据已失效");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 测试类及测试方法
@RunWith(PowerMockRunner.class)
@PrepareForTest(WxTemplateSender.class)
public class WxTemplateSenderTest {
@InjectMocks
private WxTemplateSender wxTemplateSender;
@Before
public void setUp() throws Exception {
}
@Test
public void test() throws Exception {
String[] params = new String[]{};
WxTemplateSender spy = Mockito.spy(wxTemplateSender);
PowerMockito.doNothing().when(spy, "sendMsg", "2", "2", params, "", "", "");
spy.sendWechatMsg("2", "2", params);
PowerMockito.verifyPrivate(spy, times(1)).invoke("sendMsg", "2", "2", params, "", "", "");
}
}
常见问题10:kotlin代码常见单测问题
问题:
在给kotlin代码编写单元测试时,对于方法入参为Object类型时,我们常用Mockito.any()去mock掉参数,但当要mock的方法参数不可以为空时,这个时候就可能会报npe,报错如下图所示:
解决方式:
在这种情况下,我们只能自己手动实现一个any()方法,然后去替代掉Mockito.any()方法。
private fun <T> any(): T {
Mockito.any<T>()
return uninitialized()
}
private fun <T> uninitialized(): T = null as T
三、小结
上面总结的一些大家常见的问题,其实更多是大家对单元测试不太熟悉,写的太少时候会经常碰到的。上面虽然直接给出了解决问题的方式,但是大家其实还是要弄清楚出现这些问题的原因,需要清楚Mockito+PowerMock的实现原理和常用语法,只有这样才能大大减少问题率,大大的提高我们的开发效率。
创作不易,如果有帮助到你的话请给点个赞吧!我是Wasteland,下期文章再见!