Android最佳Mock單元測試方案:Junit + Mockito + Powermock

單元測試由一組獨立的測試構成,每個測試針對軟件中的一個單獨的程序單元。單元測試并非檢查程序單元之間是否能夠合作良好,而是檢查單個程序單元行為是否正確。

為什么要進行單元測試

在敏捷開發大行其道的今天,由于時間緊,任務重,過分依賴測試工程師以及下列原因,導致單元測試不被重視,在開發流程中處于一個可有可無的尷尬境地。

  1. 浪費的時間太多
  2. 軟件開發人員不應參與單元測試
  3. 我是很棒的程序員,不需要進行單元測試
  4. 不管怎樣,集成測試將會抓住所有的Bug
  5. 單元測試效率不高

那么單元測試是否正的可有可無呢?No! No! No!

  1. 作為android客戶端研發,在一個開發周期內,你負責的需求需要Web服務(API),和本地代碼(JNI,Native Code)的支持,而你們的工作是同時進行的。
  2. 你的需求開發完成了,但是由于需要在特定條件下才能觸發,而這些條件在開發過程中很難去模擬,導致需求無法在所有場景下進行充分測試。舉個例子,假設你在室內開發一個地圖導航的Android應用,你需要在導航過程中,前方出現車禍,積水,施工等多種狀況,怎么辦?
  3. 總結你過去的BUG,你會發現有些你以為寫的很完善的邏輯,卻在最后被發現有場景未覆蓋,或者邏輯錯誤等問題。
  4. 測試工程師給你報了一個BUG,你改完提交了,但是之后由于Merge失誤導致代碼丟失,或者其他人的修改導致你的BUG再次出現。直到測試工程師再次發現該BUG,并再次給你提出。
  5. 你的開發進度很快,但是開發完成后,你會被BUG淹沒。你持續不斷的修改BUG,持續不斷的加班,直至發布版本,身心俱疲。
  6. 以前明明很正常的功能,在本次開發周期內,突然不能正常使用了。

如果你也經常碰到以上問題,或者困擾,那么你需要持續不斷的對項目進行 單元測試

Android單元測試簡介

Android的單元測試分為兩大類:

1.Instrumentation

通過Android系統的Instrumentation測試框架,我們可以編寫測試代碼,并且打包成APK,運行在Android手機上。

優點: 逼真

缺點: 很慢

代表框架:JUnit(Android自帶),espresso

2.JUnit / Mock

通過JUnit,以及第三方測試框架,我們可以編寫測試代碼,生成class文件,直接運行在JVM虛擬機中。

優點: 很快。使用簡單,方便。

缺點: 不夠逼真。比如有些硬件相關的問題,無法通過這些測試出來。

代表框架: JUnit(標準),Robolectric, mockito, powermock

Android最佳Mock單元測試方案

我通過對比前輩們對各種單元測試框架的實踐,總結出Android最佳Mock單元測試方案: Junit + Mockito + Powermock.(自己認證的…)

Junit + Mockito + Powermock 簡介

眾所周知,Junit是一個簡單的單元測試框架。

Mockito,則是一個簡單的用于Mock的單元測試框架。

那么為什么還需要Powermock呢?

EasyMock和Mockito等框架,對static, final, private方法均是不能mock的。

這些框架普遍是通過創建Proxy的方式來實現的mock。 而PowerMock是使用CGLib來操縱字節碼而實現的mock,所以它能實現對上面方法的mock。

Junit + Mockito + Powermock 引入

由于PowerMock對Mockito有較強依賴,因此需要按照以下表格采用對應的版本。

Mockito PowerMock
2.0.0-beta - 2.0.42-beta 1.6.5+
1.10.8 - 1.10.x 1.6.2+
1.9.5-rc1 - 1.9.5 1.5.0 - 1.5.6
1.9.0-rc1 & 1.9.0 1.4.10 - 1.4.12
1.8.5 1.3.9 - 1.4.9
1.8.4 1.3.7 & 1.3.8
1.8.3 1.3.6
1.8.1 & 1.8.2 1.3.5
1.8 1.3
1.7 1.2.5

建議方案:

在項目依賴文件build.gradle中添加以下依賴。

testCompile 'junit:junit:4.11'
// required if you want to use Mockito for unit tests
testCompile 'org.mockito:mockito-core:1.9.5'
// required if you want to use Powermock for unit tests
testCompile 'org.powermock:powermock-module-junit4:1.5.6'
testCompile 'org.powermock:powermock-module-junit4-rule:1.5.6'
testCompile 'org.powermock:powermock-api-mockito:1.5.6'

Junit + Mockito + Powermock 配置

  1. 默認的測試代碼位置
    對于通過gradle構建的android項目,在默認的項目結構中,Instrumentation的測試代碼放在
    src/androidTest/ 目錄,而JUnit / Mock的測試代碼放在 src/test/ 目錄。
  2. 自定義測試代碼位置
    有些項目是由Eclipse構建遷移到由Gradle構建,需要自定義測試代碼位置。
    舉個例子,androidTest和test目錄都在項目的根文件夾下。我們需要這樣配置:
    android { 
     sourceSets {
     test {
     java.srcDir 'test'
     }
     androidTest {
     java.srcDir 'androidTest'
     }
     }
    }
    

如果在單元測試中遇到類似”Method … not mocked.”的問題,請添加以下設置:

android {
// ...
 testOptions { 
 unitTests.returnDefaultValues = true
 }
}

Junit + Mockito + Powermock 使用

強烈建議你熟讀以下內容,來熟悉Junit + Mockito + Powermock的使用。

  1. Mockito 中文文檔 ( 2.0.26 beta )
  2. Mockito reference documentation
  3. powermock wiki
  4. Unit tests with Mockito - Tutorial

下面通過舉例來簡單說明Junit + Mockito + Powermock 使用,更多詳情清參考Demo項目:

https://github.com/snowdream/test/tree/master/android/test/mocktest

源碼: https://github.com/snowdream/test/blob/master/android/test/mocktest/app/src/main/java/snowdream/github/com/mocktest/Calc.java

測試代碼: https://github.com/snowdream/test/blob/master/android/test/mocktest/app/src/test/java/snowdream/github/com/mocktest/CalcUnitTest.java

1.驗證某些行為,主要是驗證某些函數是否被調用,以及被調用的具體次數。

//using mock
mockedList.add("once");

mockedList.add("twice");
mockedList.add("twice");

mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");

//following two verifications work exactly the same - times(1) is used by default
// 下面的兩個驗證函數效果一樣,因為verify默認驗證的就是times(1)
verify(mockedList).add("once");
verify(mockedList, times(1)).add("once");

//exact number of invocations verification
// 驗證具體的執行次數
verify(mockedList, times(2)).add("twice");
verify(mockedList, times(3)).add("three times");

//verification using never(). never() is an alias to times(0)
// 使用never()進行驗證,never相當于times(0)
verify(mockedList, never()).add("never happened");

//verification using atLeast()/atMost()
// 使用atLeast()/atMost()
verify(mockedList, atLeastOnce()).add("three times");
verify(mockedList, atLeast(2)).add("five times");
verify(mockedList, atMost(5)).add("three times");

2.驗證執行順序,主要驗證某些函數是否按照預定順序執行。

// A. Single mock whose methods must be invoked in a particular order
// A. 驗證mock一個對象的函數執行順序
List singleMock = mock(List.class);

//using a single mock
singleMock.add("was added first");
singleMock.add("was added second");

//create an inOrder verifier for a single mock
// 為該mock對象創建一個inOrder對象
InOrder inOrder = inOrder(singleMock);

//following will make sure that add is first called with "was added first, then with "was added second"
// 確保add函數首先執行的是add("was added first"),然后才是add("was added second")
inOrder.verify(singleMock).add("was added first");
inOrder.verify(singleMock).add("was added second");

// B. Multiple mocks that must be used in a particular order
// B .驗證多個mock對象的函數執行順序
List firstMock = mock(List.class);
List secondMock = mock(List.class);

//using mocks
firstMock.add("was called first");
secondMock.add("was called second");

//create inOrder object passing any mocks that need to be verified in order
// 為這兩個Mock對象創建inOrder對象
InOrder inOrder = inOrder(firstMock, secondMock);

//following will make sure that firstMock was called before secondMock
// 驗證它們的執行順序
inOrder.verify(firstMock).add("was called first");
inOrder.verify(secondMock).add("was called second");

// Oh, and A + B can be mixed together at will

3.使用powermock必須使用兩個annotation:

@RunWith(PowerMockRunner.class)
@PrepareForTest({Calc.class})
publicclassCalcUnitTest{
}
//PrepareForTest 后面要加準備被mock或stub的類,單個class直接()起來即可,多個用{},并用逗號隔開。

4.測試公開成員變量

@Test
publicvoidtestPublicField(){
 assertEquals(mCalc.mPublicField, 0);
 assertEquals(mCalc.mPublicFinalField, 0);
 assertEquals(Calc.mPublicStaticField, 0);
 assertEquals(Calc.mPublicStaticFinalField, 0);

 mCalc.mPublicField = 1;
 Calc.mPublicStaticField = 2;

 assertEquals(mCalc.mPublicField, 1);
 assertEquals(mCalc.mPublicFinalField, 0);
 assertEquals(Calc.mPublicStaticField, 2);
}

5.測試公開成員方法

@Test
publicvoidtestAddPublicMethod(){
//when
 when(mCalc.addPublic(anyInt(), anyInt()))
 .thenReturn(0)
 .thenReturn(1)
 .thenReturn(2)
 .thenReturn(3)
 .thenReturn(4)
 .thenReturn(5);

//call method
for(inti =0; i <6; i++) {

//verify
 assertEquals(mCalc.addPublic(i, i), i);
 }

//verify
 verify(mCalc, times(6)).addPublic(anyInt(), anyInt());
 verify(mCalc, atLeast(1)).addPublic(anyInt(), anyInt());
 verify(mCalc, atLeastOnce()).addPublic(anyInt(), anyInt());
 verify(mCalc, atMost(6)).addPublic(anyInt(), anyInt());
}

6.測試公開無返回值成員方法

@Test
publicvoidtestAddPublicVoidMethod(){
//when
 doNothing().when(mCalc).voidPublic(anyInt(), anyInt());

 mCalc.voidPublic(anyInt(), anyInt());
 mCalc.voidPublic(anyInt(), anyInt());

 verify(mCalc, atLeastOnce()).voidPublic(anyInt(), anyInt());
 verify(mCalc, atLeast(2)).voidPublic(anyInt(), anyInt());
}

7.測試公開靜態成員方法

@Test
publicvoidtestAddPublicStaicMethod()throwsException{
 PowerMockito.mockStatic(Calc.class);

 PowerMockito.when(Calc.class, "addPublicStatic", anyInt(), anyInt())
 .thenReturn(0)
 .thenReturn(1)
 .thenReturn(2)
 .thenReturn(3)
 .thenReturn(4)
 .thenReturn(5);


//call method
for(inti =0; i <6; i++) {

//verify
 assertEquals(Calc.addPublicStatic(i, i), i);
 }


//verify static
 PowerMockito.verifyStatic(times(6));
}

8.測試私有成員變量

Powermock提供了一個Whitebox的class,可以方便的繞開權限限制,可以get/set private屬性,實現注入。也可以調用private方法。也可以處理static的屬性/方法,根據不同需求選擇不同參數的方法即可。

@Test
publicvoidtestPrivateField()throwsIllegalAccessException{
 PowerMockito.mockStatic(Calc.class);

 assertEquals(Whitebox.getField(Calc.class, "mPrivateField").getInt(mCalc),0);
 assertEquals(Whitebox.getField(Calc.class, "mPrivateFinalField").getInt(mCalc),0);
 assertEquals(Whitebox.getField(Calc.class, "mPrivateStaticField").getInt(null),0);
 assertEquals(Whitebox.getField(Calc.class, "mPrivateStaticFinalField").getInt(null),0);


 Whitebox.setInternalState(mCalc, "mPrivateField",1);
 Whitebox.setInternalState(Calc.class, "mPrivateStaticField",1, Calc.class);

 assertEquals(Whitebox.getField(Calc.class, "mPrivateField").getInt(mCalc),1);
 assertEquals(Whitebox.getField(Calc.class, "mPrivateFinalField").getInt(mCalc),0);
 assertEquals(Whitebox.getField(Calc.class, "mPrivateStaticField").getInt(null),1);
 assertEquals(Whitebox.getField(Calc.class, "mPrivateStaticFinalField").getInt(null),0);
}

9.測試私有成員方法

@Test
publicvoidtestAddPrivateMethod()throwsException{
 PowerMockito.mockStatic(Calc.class);

//when
 PowerMockito.when(mCalc,"addPrivate",anyInt(),anyInt())
 .thenReturn(0)
 .thenReturn(1)
 .thenReturn(2)
 .thenReturn(3)
 .thenReturn(4)
 .thenReturn(5);

//call method
for(inti =0; i <6; i++) {

//verify
 assertEquals(Whitebox.invokeMethod(mCalc,"addPrivate",i,i), i);
 }

//verify static
 PowerMockito.verifyPrivate(mCalc,times(6)).invoke("addPrivate",anyInt(),anyInt());
 PowerMockito.verifyPrivate(mCalc,atLeast(1)).invoke("addPrivate",anyInt(),anyInt());
 }

10.測試私有靜態成員方法

@Test
publicvoidtestAddPrivateStaicMethod()throwsException{
 PowerMockito.mockStatic(Calc.class);

 PowerMockito.when(Calc.class, "addPrivateStatic", anyInt(), anyInt())
 .thenReturn(0)
 .thenReturn(1)
 .thenReturn(2)
 .thenReturn(3)
 .thenReturn(4)
 .thenReturn(5);


//call method
for(inti =0; i <6; i++) {

//verify
 assertEquals(Whitebox.invokeMethod(Calc.class,"addPrivateStatic",i, i), i);
 }


//verify static
 PowerMockito.verifyStatic(times(6));
 }

通過以上介紹,相信你對Android項目的Mock單元測試有一定的了解。

如果你有任何相關疑問,請通過以下方式聯系我:

Email:yanghui1986527#gmail.com

QQ 群: 529327615

參考

  1. 詳細講解單元測試的內容
  2. 淺談單元測試的意義
  3. 敏捷開發之測試
  4. Unit testing support
  5. junit4
  6. mockito
  7. powermock
  8. mocktest
  9. 在Android Studio中進行單元測試和UI測試
  10. whats-the-best-mock-framework-for-java
  11. mock測試
  12. 使用PowerMock來Mock靜態函數
  13. PowerMock介紹
  14. Sharing code between unit tests and instrumentation tests on Android
  15. Unit tests with Mockito - Tutorial
  16. Android單元測試之Mockito淺析
  17. Mockito 簡明教程
  18. mockito簡單教程

 

來自:http://snowdream.github.io/blog/2016/08/03/android-mock-test/

 

 本文由用戶 hongxinkg1 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!