淺談測試之PowerMock

PowerMock的簡介

PowerMock官網java

編寫單元測試僅靠Mockito是不夠。由於Mockito沒法mock私有方法、final方法及靜態方法等。android

PowerMock這個framework,主要是爲了擴展其餘mock框架,如Mockito、EasyMock。它使用一個自定義的類加載器,纂改字節碼,突破Mockito沒法mock靜態方法、構造方法、final類、final方法以及私有方法的限制。git

不過,聽起來很牛逼。但不少時候你也能夠不用它。有一種簡單的方法,能夠替代它,好比,你能夠提升那些成員變量、成員方法的可見性,而後,用@VisibleForTesting標註該變量或方法。對,就這麼簡單。android源碼也是這麼幹的。你能夠在源碼裏看見很多這樣的註解。github

不過若是你要mock的是一些靜態方法或者第三方庫的私有方法,那隻能含淚使用PowerMock了。api

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的版本相對應。bash

參考資料: Using PowerMock with Mockito

PowerMock的使用

1.mock變量

PowerMock能夠mock變量。並非什麼黑科技,其實就是經過Java反射修改變量。PowerMock只是對Java反射進行了封裝,提供相應的API,方便咱們使用。相應的方法,就在WhiteBox這個類裏,下面列出兩個常用的方法:app

//反射修改某個變量的值。修改靜態變量的值時,第一個參數傳相應的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源碼裏的測試用例。

相關文章
相關標籤/搜索