浅谈测试之PowerMock

7,157 阅读8分钟

PowerMock的简介

PowerMock官网

编写单元测试仅靠Mockito是不够。因为Mockito无法mock私有方法、final方法及静态方法等。

PowerMock这个framework,主要是为了扩展其他mock框架,如Mockito、EasyMock。它使用一个自定义的类加载器,纂改字节码,突破Mockito无法mock静态方法、构造方法、final类、final方法以及私有方法的限制。

不过,听起来很牛逼。但很多时候你也可以不用它。有一种简单的方法,可以替代它,比如,你可以提高那些成员变量、成员方法的可见性,然后,用@VisibleForTesting标注该变量或方法。对,就这么简单。android源码也是这么干的。你可以在源码里看见不少这样的注解。

不过如果你要mock的是一些静态方法或者第三方库的私有方法,那只能含泪使用PowerMock了。

PowerMock的集成

1.app的build.gradle下添加依赖

testImplementation "org.powermock:powermock-core:2.0.2"
testImplementation "org.powermock:powermock-module-junit4:2.0.2"
//配合mockito使用需要添加
testImplementation "org.powermock:powermock-api-mockito2:2.0.2"

注意:在使用PowerMock作为对Mockito的测试补充框架时,PowerMock的版本,要与使用的Mockito的版本相对应。

参考资料:Using PowerMock with Mockito

PowerMock的使用

1.mock变量

PowerMock可以mock变量。并不是什么黑科技,其实就是通过Java反射修改变量。PowerMock只是对Java反射进行了封装,提供相应的API,方便我们使用。相应的方法,就在WhiteBox这个类里,下面列出两个经常使用的方法:

//反射修改某个变量的值。修改静态变量的值时,第一个参数传相应的Class即可。
public static void setInternalState(Object object, String fieldName, Object value)
//反射获取某个变量的值。获取静态变量的值时,第二个参数传相应的Class即可。
public static Object getFieldValue(final Field field, final Object object)

注意:PowerMock既然是通过反射修改对应的值,那么就意味着无法对被final修饰的基本类型和String类型变量进行mock(注意:不包括基本类型的包装类型)。至于为什么,文末也会对这个问题进行探讨。

2.mock方法

WhiteBox里面也封装了相应的方法,方便我们通过反射调用私有方法。当然,这不是重点,只是充下字数。

//用于反射调用成员方法
public static synchronized <T> T invokeMethod(Object instance, String methodToExecute, Object... arguments)
//用于反射调用静态方法
public static synchronized <T> T invokeMethod(Class<?> clazz, String methodToExecute, Object... arguments) 

由于PowerMock是在Mockito的基础上扩展的框,所以它的很多API与Mockito类似。但又有不少区别。PowerMock能mock成员方法,包括私有的,也能mock静态方法。

PowerMock在进行mock方法时,需要在先使用下面的注解:

@RunWith(PowerMockRunner.class)
//需要在里面声明所有要mock的类
@PrepareForTest(PowerMockSample.class)
public class PowerMockSampleTest {
}

PowerMock提供的mock方法,大致可分为mock(Class<T> type)、spy(T object)、mockStatic(Class<?> type, Class<?>... types)、spy(Class<T> type)四种方法。前两者用于mock成员方法,后两者则是用于mock静态方法。所以前两者是会返回相应mock实例,而后两者则没有返回值。

在PowerMock里,spy和mock的区别,基本跟Mocktio是类似的。所以,请参考之前关于Mockito的博文,不再赘述。这里,只强调一下:

经由spy(T object)产生的mock实例,通过when...thenReturn...来mock一个方法。然后用该mock实例调用该方法(或者尝试调用该类的该静态方法)时,在返回指定值之前,会走真实逻辑。但通过doReturn...when...来mock一个方法,则不会走真实逻辑。

调用spy(Class<T> type)后,通过when...thenReturn...来mock一个静态方法。然后在调用该静态方法时,在返回指定值之前,会走真实逻辑。但通过doReturn...when...来mock一个静态方法,则不会走真实逻辑。

1)mock成员方法

mock(Class<T> type)、spy(T object)均会生成一个mock实例。只有mock实例,才能调用PowerMock的API来mock成员方法。

@Test
public void spyObject_mockPrivateMethodCalculateThrowException() throws Exception {
    int expected = 10;
    powerMockSample = PowerMockito.spy(powerMockSample);
    //不要使用when(...).thenReturn(...)。会调用你想要mock的方法的真实逻辑。然后才返回mock的结果。
    //doReturn方法的注释,也提供了相关解释和例子。
    //when(powerMockSample, "privateMethodCalculateThrowException", isA(int.class), isA(int.class)).thenReturn(expected);
    doReturn(expected).when(powerMockSample, "privateMethodCalculateThrowException", isA(int.class), isA(int.class));
    int actual = Whitebox.invokeMethod(powerMockSample, "privateMethodCalculateThrowException", 1, 2);
    assertEquals(expected, actual);
}

@Test
public void mockClass_mockPrivateMethodCalculateThrowException() throws Exception {
    int expected = 10;
    powerMockSample = PowerMockito.mock(PowerMockSample.class);
    //两者均可。均不会走真实逻辑。
    //when(powerMockSample,"privateMethodCalculateThrowException", isA(int.class), isA(int.class)).thenReturn(expected);
    doReturn(expected).when(powerMockSample, "privateMethodCalculateThrowException", isA(int.class), isA(int.class));
    int actual = Whitebox.invokeMethod(powerMockSample, "privateMethodCalculateThrowException", 1, 2);
    assertEquals(expected, actual);
}

2)mock静态方法

mockStatic(Class<?> type, Class<?>... types)、spy(Class<T> type)

@Test
public void mockStatic_mockPublicStaticMethodReturnStringButThrowException() throws Exception {
    String newValue = "mockPublicStaticMethodReturnStringButThrowException";

    PowerMockito.mockStatic(PowerMockSample.class);

    //没有抛异常,所以没有走真实逻辑。返回了null。
    assertEquals(null, PowerMockSample.publicStaticMethodReturnStringButThrowException());

    //when...thenReturn...也是一样的效果。不会走真实逻辑。
    //两种when写法没什么区别。
    //when(PowerMockSample.class,"publicStaticMethodReturnStringButThrowException").thenReturn(newValue);
    //when(PowerMockSample.publicStaticMethodReturnStringButThrowException()).thenReturn(newValue);

    //错误的doReturn写法。会抛UnfinishedStubbingException异常。
    //doReturn(newValue).when(PowerMockSample.publicStaticMethodReturnStringButThrowException());
    doReturn(newValue).when(PowerMockSample.class,"publicStaticMethodReturnStringButThrowException");
    assertEquals(newValue, PowerMockSample.publicStaticMethodReturnStringButThrowException());
}


@Test
public void spyClass_mockPublicStaticMethodNoReturnThrowException() throws Exception {
    PowerMockito.spy(PowerMockSample.class);
    boolean isThrowException = false;
    try {
        Whitebox.invokeMethod(PowerMockSample.class, "privateStaticMethodNoReturnThrowException");
    } catch (Exception e) {
        isThrowException = true;
    }
    //抛异常,执行了真实逻辑。
    assertEquals(true,isThrowException);

    //传的是mock过的class
    //没有返回值的方法,只能通过doNothing...when...
    doNothing().when(PowerMockSample.class, "publicStaticMethodNoReturnThrowException");
    PowerMockSample.publicStaticMethodNoReturnThrowException();
}

注意:

1.跟Mockito里的spy(Class<T> type)不同,spy(Class<T> type)是没有返回值的。它被命名为spyStatic的话,会更恰当一点。因为它跟mockStatic一样,是在mock静态方法时会用到的API。

2.调用doReturn...when...来mock一个静态方法时,不要通过类名直接调用该方法,如:

//会抛UnfinishedStubbingException异常
doReturn(expected).when(PowerMockSample.publicStaticMethodCalculate(isA(int.class),isA(int.class)));

正确的做法如下:

doReturn(expected).when(PowerMockSample.class, "publicStaticMethodCalculate", isA(int.class), isA(int.class));

反射真的无法修改被final修饰的基本类型和String类型变量?

前面提到PowerMock是通过反射修改对应的值来达到mock的目的。这也就意味着无法对被final修饰的基本类型和String类型变量进行mock。

这点,追踪PowerMock的WhiteBox.setInternalState()方法的源码也可以发现。

private static void checkIfCanSetNewValue(Field fieldToSetNewValueTo) {
    int fieldModifiersMask = fieldToSetNewValueTo.getModifiers();
    boolean isFinalModifierPresent = (fieldModifiersMask & Modifier.FINAL) == Modifier.FINAL;
    boolean isStaticModifierPresent = (fieldModifiersMask & Modifier.STATIC) == Modifier.STATIC;

    if(isFinalModifierPresent && isStaticModifierPresent){
        boolean fieldTypeIsPrimitive = fieldToSetNewValueTo.getType().isPrimitive();
        if (fieldTypeIsPrimitive) {
        throw new IllegalArgumentException("You are trying to set a private static final primitive. Try using an object like Integer instead of int!");
        }
        boolean fieldTypeIsString = fieldToSetNewValueTo.getType().equals(String.class);
        if (fieldTypeIsString) {
            throw new IllegalArgumentException("You are trying to set a private static final String. Cannot set such fields!");
        }
    }
}

但这段代码,也存在一定的问题:

1)相关代码里不涉及修饰符private,抛出的异常信息有误导

2)关键是final修饰的基本类型、String类型变量都无法通过反射修改来达到mock的目的。跟是不是static没啥关系。

3)还有,如果测试类加上注解@RunWith(PowerMockRunner.class),该异常无法正常抛出,应该是被try catch了。

但是反射真的无法修改被final修饰的基本类型和String类型变量?

我们先来看下面这段测试:

@Test
public void mockPublicStaticFinalInt() {
    //public static final int publicStaticFinalInt = 1;
    int newValue = 2;
    Whitebox.setInternalState(PowerMockSample.class, "publicStaticFinalInt", newValue);
    //注意,这里反射修改成功了
    //直接通过反射获取变量的值
    assertEquals(newValue, getStaticFieldValue(PowerMockSample.class, "publicStaticFinalInt"));
    //注意,这里并不相等。
    assertNotEquals(newValue, PowerMockSample.publicStaticFinalInt);
}

为什么变量publicStaticFinalInt的值。明明被修改了,结果却不相等?因为assertNotEquals(newValue, PowerMockSample.publicStaticFinalInt);这段代码里的PowerMockSample.publicStaticFinalInt在编译时,在字节码里已经被替换成相应的值。所以,变量publicStaticFinalInt的值再怎么修改。都与它无关。

我们接着看另外一段代码:

    int mInt = 1;
    String mString = "string";
    static int mStaticInt = 1;
    static String mStaticString = "staticString";
    final int mFinalInt = 1;
    final String mFinalString = "finalString";
    final static int mFinalStaticInt = 1;
    final static String mFinalStaticString = "finalStaticString";

    public void test() {
        int _int = mInt;
        String string = mString;
        int staticInt = mStaticInt;
        String staticString = mStaticString;
        int finalInt = mFinalInt;
        String finalString = mFinalString;
        int finalStaticInt = mFinalStaticInt;
        String finalStaticString = mFinalStaticString;
    }

test()方法对应的字节码:

public void test();
    Code:
       0: aload_0
       1: getfield      #2                  // Field mInt:I
       4: istore_1
       5: aload_0
       6: getfield      #4                  // Field mString:Ljava/lang/String;
       9: astore_2
      10: getstatic     #8                  // Field mStaticInt:I
      13: istore_3
      14: getstatic     #9                  // Field mStaticString:Ljava/lang/String;
      17: astore        4
      19: iconst_1
      20: istore        5
      22: ldc           #6                  // String finalString
      24: astore        6
      26: iconst_1
      27: istore        7
      29: ldc           #11                 // String finalStaticString
      31: astore        8
      33: return

仔细看看test()方法的字节码,非final修饰的变量,比如mInt、mString、mStaticInt、mStaticString,在赋值前,都是通过字节码指令getfield或者getstatic获取对应的实例变量、类变量的值。而final修饰的变量mFinalInt 、mFinalStaticInt在赋值前,通过字节码指令iconst_1加载对应的值1,变量mFinalString、mFinalStaticString通过字节码指令ldc,直接从常量池中加载对应的值,跟对应的实例变量、类变量不相干。

所以,问题的答案应该是:反射其实是能修改某个被final修饰的基本类型或者String类型变量,让它指向新的值。但却无法修改其他使用到该变量的代码里的值。因为在那些代码里,该变量的值在编译期就已经被直接替换成了对应的值,或者指向常量池里的某个常量。

后记

PowerMock弥补了Mockito不能mock私有方法、静态方法、final方法的缺陷,方便我们在不破坏代码封装的情况下,写测试用例。

文中的相关测试例子,以及更多的测试例子均可以在UnitTest里面找到。

更多的测试例子,以及相关API的使用方法,请参考PowerMock源码里的测试用例。