Android單元測(cè)試在復(fù)雜項(xiàng)目里的落地姿勢(shì)(PowerMock實(shí)踐篇)

代碼出處:colin-phang/AndroidUnitTest

上篇文章:Android單元測(cè)試在復(fù)雜項(xiàng)目里的落地姿勢(shì)(調(diào)研篇)

上篇《調(diào)研》的結(jié)論是:

  1. Espresso需要跑在真機(jī)上,可用于依賴Android平臺(tái)的功能測(cè)試。
  2. Roboelctric問(wèn)題太多在復(fù)雜項(xiàng)目中寸步難行,棄了。
  3. 考慮PowerMockito來(lái)隔離整個(gè)Android SDK以及項(xiàng)目業(yè)務(wù)的依賴,來(lái)保證單元測(cè)試代碼能夠快速有效地編寫(xiě)并執(zhí)行。

文章主要分成 調(diào)研、 實(shí)踐 兩篇。 本篇主要講講基于PowerMockito如何在項(xiàng)目進(jìn)行Android單元測(cè)試的實(shí)踐。

1 依賴

參考:powermock/wiki

testImplementation 'junit:junit:4.12'
testImplementation "org.powermock:powermock-module-junit4:2.0.2"
testImplementation "org.powermock:powermock-api-mockito2:2.0.2"

按照上述引入PowerMock的依賴后即可在項(xiàng)目test目錄下使用PowerMockito和Mockito了。

2 使用

0 基本使用

@RunWith(PowerMockRunner.class)
@PrepareForTest( { YourClassWithEgStaticMethod.class })
public class YourTestCase {
...
}
  1. @RunWith,使測(cè)試代碼運(yùn)行于PowerMockRunner的環(huán)境下。
  2. @PrepareForTest,當(dāng)需要Mock某個(gè)類的static、final、private方法的時(shí)候,就需要聲明該注解。

上一篇提到,結(jié)合PowerMockito編寫(xiě)單元測(cè)試代碼,遵循以下三個(gè)步驟:

  1. Mock被依賴的復(fù)雜對(duì)象
  2. 執(zhí)行被測(cè)代碼
  3. 驗(yàn)證邏輯是否按照預(yù)期執(zhí)行/返回

而單元測(cè)試用例的編寫(xiě),一部分取決于對(duì)業(yè)務(wù)代碼的熟悉程度,另一方面則取決于對(duì)單元測(cè)試框架的了解程度,以下框架的很多用法具體還是需要自己去搜索資料并掌握的,
具體可以參考這兩個(gè)文檔:

  1. hehonghui/mockito-doc-zh
  2. powermock/powermock

上篇文章也有一個(gè)簡(jiǎn)單的示例:PowerMockito在Android單元測(cè)試中的簡(jiǎn)單使用,這里不再贅述,下面說(shuō)說(shuō)在編寫(xiě)單元測(cè)試代碼過(guò)程中,如何借助PowerMockito隔離Android SDK的依賴。

1 創(chuàng)建模擬對(duì)象的2種姿勢(shì)

mock

activity = PowerMockito.mock(new MainActivity())
//使activity的isFinishing方法總是返回true
when(activity.isFinishing()).thenReturn(true);

通過(guò)mock創(chuàng)造出來(lái)的對(duì)象,調(diào)用該對(duì)象所有方法都不會(huì)執(zhí)行真實(shí)邏輯。必須結(jié)合when(...).then(...)來(lái)使模擬對(duì)象按照我們預(yù)期返回。

spy

activity = PowerMockito.spy(new MainActivity())
//使activity的isFinishing方法總是返回false
PowerMockito.doReturn(false).when(activity).isFinishing();

通過(guò)spy創(chuàng)造模擬對(duì)象必須先手動(dòng)new出來(lái),調(diào)用該對(duì)象所有方法都會(huì)執(zhí)行真實(shí)邏輯。
spy對(duì)象必須結(jié)合doReturn(...).when(...)才會(huì)忽略真實(shí)邏輯,并按照我們預(yù)期返回。

如果函數(shù)返回值為void,可以用doNothing()代替doReturn()

2 訪問(wèn)/調(diào)用private

參考 powermock/wiki/Bypass-Encapsulation

有時(shí)候被測(cè)類絕大部分是private函數(shù)(比如Activity),傳統(tǒng)的單元測(cè)試很難覆蓋到這些private函數(shù),當(dāng)然我們可以通過(guò)重構(gòu)/封裝使我們的業(yè)務(wù)代碼對(duì)測(cè)試更友好,但為了測(cè)試而對(duì)原本穩(wěn)定的業(yè)務(wù)代碼進(jìn)行侵入式的修改,在短期內(nèi)肯定會(huì)帶來(lái)不穩(wěn)定因素,這往往是團(tuán)隊(duì)/領(lǐng)導(dǎo)無(wú)法容忍的。

PowerMock的Whitebox類提供了一組api可以獲取/修改private的變量和函數(shù),可以幫助我們繞過(guò)重構(gòu)去對(duì)業(yè)務(wù)代碼進(jìn)行測(cè)試。

//修改私有變量
Whitebox.setInternalState(..)
//訪問(wèn)私有變量
Whitebox.getInternalState(..)
//調(diào)用私有函數(shù)
Whitebox.invokeMethod(..)
//調(diào)用私有的構(gòu)造函數(shù)
Whitebox.invokeConstructor(..) 

非靜態(tài)內(nèi)部類的對(duì)象會(huì)隱式持有外部類對(duì)象,所以mock非靜態(tài)內(nèi)部類,需要給”this$0“的成員變量賦值,不然單元測(cè)試代碼運(yùn)行時(shí)會(huì)報(bào)錯(cuò)。

Whitebox.setInternalState(innerObj, "this$0", outerObj)

3 抑制不必要的代碼邏輯執(zhí)行

在實(shí)際項(xiàng)目中會(huì)有很多常用但不影響業(yè)務(wù)邏輯的代碼(Log以及其他統(tǒng)計(jì)代碼等等),有些靜態(tài)代碼塊也直接調(diào)用Android SDK api。因?yàn)閱卧獪y(cè)試代碼運(yùn)行在JVM上,這些代碼很容易會(huì)報(bào)錯(cuò),如果為了測(cè)試去修改這些代碼未免有點(diǎn)本末倒置,所以我們?cè)趩卧獪y(cè)試的過(guò)程中需要抑制/隔離這些代碼的執(zhí)行。

抑制靜態(tài)變量/代碼塊的執(zhí)行

PowerMockito提供了@SuppressStaticInitializationFor注解:

//在單元測(cè)試類之前聲明以下注解,可以阻止FileUtil類的靜態(tài)代碼塊運(yùn)行
@SuppressStaticInitializationFor("com.colin.unittest.FileUtil")
public class PowerMockitoSampleIII {
    ...
}

抑制Log等靜態(tài)函數(shù)的執(zhí)行

借助mockStatic可以使指定類的靜態(tài)方法不執(zhí)行。

@PrepareForTest(Log.class)
public class PowerMockitoSampleIII {
    @Before
    public void setUp() throws Exception {
    //抑制Log相關(guān)代碼的執(zhí)行
    PowerMockito.mockStatic(Log.class);
    }
    ...
}

抑制super函數(shù)()的執(zhí)行

實(shí)際業(yè)務(wù)開(kāi)發(fā)中,我們經(jīng)常需要繼承Android SDK的類來(lái)進(jìn)行擴(kuò)展,對(duì)這些類覆寫(xiě)的函數(shù)進(jìn)行單元測(cè)試時(shí),往往需要抑制父類super()的邏輯,不然在JVM中執(zhí)行單元測(cè)試代碼時(shí)會(huì)報(bào)錯(cuò)。

//抑制MainActivity父類的onDestroy方法
Method method = PowerMockito.method(MainActivity.class.getSuperclass(),
    "onDestroy");
PowerMockito.suppress(method);

3 結(jié)論

綜上所述,在Android單元測(cè)試中,通過(guò)PowerMockito來(lái)隔離整個(gè)Android SDK以及項(xiàng)目業(yè)務(wù)的依賴,將單元測(cè)試的重心放在較細(xì)粒度(函數(shù)級(jí)別)的代碼邏輯,完全可行。

4 一些問(wèn)題

  1. 單元測(cè)試覆蓋率:
    使用了PowerMock的@PrepareForTest修飾的類單元測(cè)試覆蓋率變成0。這個(gè)問(wèn)題暫時(shí)沒(méi)看到解決方案。

5 參考文章

  1. 【騰訊TMQ】用Powermock和Mockito來(lái)做安卓單元測(cè)試
  2. 【美團(tuán)技術(shù)團(tuán)隊(duì)】Android單元測(cè)試研究與實(shí)踐
  3. Android 單元測(cè)試實(shí)戰(zhàn)(1)—— 調(diào)研與選型
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容