PowerMock踩坑指南
单元测试踩过无数的坑,都源于源代码写的不好,但是如何在不修改源代码的同时把单元测试写好,是我这个实习生应该摸索的,下面把我遇到的坑和PowerMock的一些方法做个总结。
首先贴个官方文档:
https://github.com/powermock/powermock
所有用到的方法文档里都有,关键在于如何组合,如果使用。
一般来说,如果你使用到PowerMock框架来进行单元测试的时候,一定是你遇到了待测试的类是一个静态类,或者待测试的类里有一堆静态方法,又或者里面含有final、私有方法等,那么这时候PowerMock能帮上很多忙。
首先在maven中添加依赖,然后在测试类中使用@RunWith(PowerMockRunner.class)注解。
第一类坑:待测的类中的某方法中调用了其他类的静态方法。
这是最简单的坑,只要使用:
mockStatic(Astatic.class)
Astatic astiatic = mock(Astatic.class)
when(astatic.对应的静态方法()).thenReturn();
第二类坑:待测试类初始化时自带静态方法。
public class A{
private OkHttpClient httpClient = new OkHttpClient();
private String str = XXUtil.getinstance().getValue();
}
XXUtil类自身并不是一个静态类,但它的初始化中也含有静态变量以及它的getinstance()方法也是一个静态方法。而OkHttpClient中也存在大量方法,如果依旧使用@InjectMocks注解对类A进行注入则会报以下错误:
java.lang.RuntimeException: Invoking the beforeTestMethod method on PowerMock test listener org.powermock.api.extension.listener.AnnotationEnabler@6d91790b failed.
为了解决这个错误,我们需要做以下几步:
1.首先我们要知道,单元测试仅仅测试当前类,只要调用了其他类的方法,那么那些类都要统统Mock掉。
2.其次,不要注释注入的形式来创建测试类,要使用最原始创建类的方法。
@RunWith(PowerMockRunner.class)
@PrepareForTest({XXUtile.class,OkHttpClient.class})
@PowerMockIgnore({ "javax.xml.*",
"javax.management.*","com.sun.org.apache.xerces.*","javax.net.ssl.*"})
public class A{
@Test
public void Amethod(){
A a = new A();
ReflectionTestUtils.setField(a,"str","abc");
}
}
当你需要使用PowerMock强大功能(Mock静态、final、私有方法等)的时候,就需要加注解@PrepareForTest,将所有的需要Mock的类都填上.
@PowerMockIgnore这个注解很奇怪,如果不加,就有可能报以下错误:
java.lang.IllegalAccessError: class javax.xml.parsers.FactoryFinder (in unnamed module @0x5ec77191) cannot access class jdk.xml.internal.SecuritySupport (in module java.xml) because module java.xml does not export jdk.xml.internal to unnamed module @0x5ec77191
网上查到的解释也不甚清楚,个人理解为PowerMock会根据你的mock要求,去修改写在注解@PrepareForTest里的class文件(当前测试类会自动加入注解中),以满足特殊的mock需求。对应的框架尝试使用反射实例化类,并从线程上下文类加载器(PowerMock的类加载器)执行此操作,但随后尝试将创建的对象分配给未由同一类加载器加载的字段。所以需要@PowerMockIgnore注释来告诉PowerMock将某个包的加载推迟到系统类加载器。您需要忽略的是特定于案例,但通常是XML框架或与其交互的一些包。
随后不能使用InjectMocks注入,要在测试方法中实例化测试类,并通过反射的方法对之前抑制初始化的参数赋值。
注意,如果类初始化中的参数实例化使用的XXUtile类中的构造函数若为私有,则需使用suppress(constructor(XXUtile.class))进行抑制,否则会报错。
@RunWith(PowerMockRunner.class):在测试用例的类级别使用注释。@PrepareForTest(ClassWithEvilParentConstructor.class):在测试用例的类级别结合使用注释,suppress(constructor(EvilParent.class))以抑制EvilParent类的所有构造函数。使用该Whitebox.newInstance(ClassWithEvilConstructor.class):方法实例化一个类而不调用构造函数使用@SuppressStaticInitializationFor("org.mycompany.ClassWithEvilStaticInitializer"):注释删除类的静态初始化程序org.mycompany.ClassWithEvilStaticInitializer。@PrepareForTest(ClassWithEvilMethod.class):在测试用例的类级别结合使用注释,suppress(method(ClassWithEvilMethod.class, "methodName"))以在ClassWithEvilMethod类中禁止名为“methodName”的方法。@PrepareForTest(ClassWithEvilField.class):在测试用例的类级别结合使用注释,suppress(field(ClassWithEvilField.class, "fieldName"))以在ClassWithEvilField类中禁止名称为“fieldName”的字段。
这是官方文档中给出的几种抑制方法,视情况合理使用。
第三类坑:我们希望Mock的一个类,定义了一个static块,其中又调用了私有的静态方法。在这个私有静态方法中,依赖了其他的一些对象,这些对象还牵扯到服务容器的问题。即使以静态的方式Mock了该类,仍然逃不过运行static块的命运,换言之,仍然需要依赖服务容器。
这时候我们可以使用@SuppressStaticInitializationFor,在该注解中需要传入字符串类型的目标类型的全名。
注意,该注解不能对测试类进行使用,否则会导致单元测试覆盖率为0的情况产生
第四类坑:需要做单元测试的类自身是静态类,或含静态方法,或两者皆有之
这个问题的解决办法其实和第二类坑差不多,只不过强调一点:
String s = XXX.builder().
name().
build().
create();
在遇到这一类方法时,要对每一层都进行mock,因为会依赖与XXX这个类,所以建议直接将这个类的方法抽象出来
private void mockXXXBuilder(String S) {
XXX.Builder XXXBuilderMock = mock(XXX.Builder.class);
when(XXXBuilderMock.build()).thenReturn(XXXBuilderMock);
when(XXXBuilderMock.name(anyString())).thenReturn(XXXBuilderMock);
when(XXX.builder()).thenReturn(XXXBuilderMock);
when(XXXBuilderMock.create(anyString())).thenReturn(XXXBuilderMock);
诸如此。
最后强调一点,若mock静态类,可以采用以下这种形式:
mockStatic(XXXUtil.class);
String str = mock(String.class)
when(String.format(any(),any(),any())).thenReturn(str);
---------------------------------------------------------------------------
PowerMockito.mockStatic(String.class);
when(String.format(any(),any(),any())).thenReturn("abcdefg");
两种方法,自由选择。