Android單元測(cè)試(二)-實(shí)戰(zhàn)

一、項(xiàng)目單元測(cè)試環(huán)境配置

gradle配置:

dependencies {
    // junit4
    testImplementation 'junit:junit:4.12'
    // mokito
    testImplementation "org.mockito:mockito-core:2.8.9"
    testImplementation "org.mockito:mockito-android:2.8.9”
  
    // powermokito
    testImplementation "org.powermock:powermock-module-junit4:1.7.1"
    testImplementation "org.powermock:powermock-api-mockito2:1.7.1"
    testImplementation 'org.powermock:powermock-core:1.7.1'
    testImplementation "org.powermock:powermock-module-junit4-rule:1.7.1"
    testImplementation "org.powermock:powermock-classloading-xstream:1.7.1"
   
    //robolectric
    testImplementation "org.robolectric:robolectric:3.3.1"
}

抽取單測(cè)基類:

1)純java單測(cè)基類

@RunWith(RobolectricTestRunner.class)
@Config(manifest = "AndroidManifest.xml", sdk = 21, application = ApplicationStub.class)
public abstract class BaseJavaTest {

    private int androidSdkVersion;

    protected static void log(String msg) {
        System.out.println(msg);
    }

    @Before
    public void init() {
        // 輸出日志
        MockitoAnnotations.initMocks(this);
        androidSdkVersion = VERSION.SDK_INT;
    }

    /**
     * hook執(zhí)行前,測(cè)試log
     */
    @Before
    public void startLog() {
        log("======= start =======");
    }

    /**
     * hook執(zhí)行后,測(cè)試log
     */
    @After
    public void endLog() throws NoSuchFieldException, IllegalAccessException {
        log("======= end =======");
        mockVersionSdkIntReturn(androidSdkVersion);
    }

    //基類可以封裝一些通用的單測(cè)方法

    /**
     * mock sdk 版本
     */
    protected void mockVersionSdkIntReturn(int apiVersion) throws NoSuchFieldException, IllegalAccessException {
        Field sdkInt = VERSION.class.getField("SDK_INT");
        sdkInt.setAccessible(true);
        sdkInt.set(null, apiVersion);
    }
  ...
}

2)PowerMock基類

@RunWith(RobolectricTestRunner.class)
@Config(manifest = "AndroidManifest.xml", sdk = 21, application = ApplicationStub.class)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*", "org.powermock.*"})
public abstract class BasePowerMockTest {

    private int androidSdkVersion;

         //解決powermock和robolectric兼容性
    @Rule
    public PowerMockRule rule = new PowerMockRule();

    protected static void log(String msg) {
        System.out.println(msg);
    }

    @Before
    public void init() {
        // 輸出日志
        MockitoAnnotations.initMocks(this);
        androidSdkVersion = VERSION.SDK_INT;
    }

    /**
     * hook執(zhí)行前,測(cè)試log
     */
    @Before
    public void startLog() {
        log("======= start =======");
    }

    /**
     * hook執(zhí)行后,測(cè)試log
     */
    @After
    public void endLog() {
        log("======= end =======");
        try {
            mockVersionSdkIntReturn(androidSdkVersion);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    protected void mockVersionSdkIntReturn(int apiVersion) throws NoSuchFieldException, IllegalAccessException {
        Field sdkInt = VERSION.class.getField("SDK_INT");
        sdkInt.setAccessible(true);
        sdkInt.set(null, apiVersion);
    }
  ...
}

二、可測(cè)代碼設(shè)計(jì)原則

  • 充分利用mvp模式;
  • 一個(gè)函數(shù)只做一件事情,不要把幾件事揉在一起;
  • 通過將查詢和行為分離,可以方便對(duì)查詢做測(cè)試;
  • 將純邏輯部分拆分為獨(dú)立函數(shù),方便測(cè)試;
  • 函數(shù)的依賴盡量通過參數(shù)來傳遞;
  • 盡量不要把復(fù)雜結(jié)構(gòu)當(dāng)做參數(shù)傳遞給函數(shù)。

好的代碼,能極大的避免mock,降低單測(cè)書寫難度,因此單測(cè)某種程度上也能反過來倒逼程序員寫出更優(yōu)秀的代碼。

三、Android單測(cè)實(shí)戰(zhàn)場(chǎng)景

mock場(chǎng)景的單測(cè)主要包括三大步:構(gòu)建對(duì)象 + 打樁 + 驗(yàn)證行為

3.1 構(gòu)建對(duì)象
  • new 常規(guī)初始化對(duì)象
  • mock 構(gòu)建空實(shí)現(xiàn)對(duì)象
  • spy 構(gòu)建具體實(shí)現(xiàn)對(duì)象

注:
類沒有依賴且對(duì)象好構(gòu)建,那可以選擇new來初始化對(duì)象,否則使用mock/spy。前者類對(duì)外部依賴較多,只關(guān)新少數(shù)函數(shù)的具體實(shí)現(xiàn);后者類對(duì)外依賴較少,關(guān)心大部分函數(shù)的具體實(shí)現(xiàn)。

3.2 打樁

mock對(duì)象之后的后續(xù)函數(shù)操作,doCallRealMethod()、doReturn()、thenReturn()、doNothing()等是比較常用的打樁方法。

1)public/protected/default方法:

mock類執(zhí)行真實(shí)方法

AppManagerModule appManagerModule = Mockito.mock(AppManagerModule.class);
Mockito.doCallRealMethod().when(appManagerModule).isIgnoreApp("22");
boolean res = appManagerModule.isIgnoreApp("22");
Assert.assertFalse(res);

公有方法返回修改值

ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);

Context context = Mockito.mock(Context.class);
ActivityManager activityManager = Mockito.mock(ActivityManager.class);
Mockito.doReturn(activityManager).when(context).getSystemService(Mockito.eq(Context.ACTIVITY_SERVICE));
2)private/static/final方法

mock類執(zhí)行真實(shí)方法

AppManagerModule mockAppManagerModule = PowerMockito.mock(AppManagerModule.class);
doCallRealMethod().when(mockAppManagerModule).getDownloadingApkCount();
mockAppManagerModule.getDownloadingApkCount();

靜態(tài)方法返回修改值

List<DownloadInfo> downloadInfoList = DownloadProxy.getInstance().getDownloadInfoList(DownloadType.APK, true);

DownloadProxy downloadProxy = PowerMockito.mock(DownloadProxy.class);
//類的@PrepareForTest需要添加DownloadProxy.class
PowerMockito.mockStatic(DownloadProxy.class);
//DownloadProxy.getInstance()默認(rèn)返回mock的downloadProxy實(shí)例
PowerMockito.when(DownloadProxy.getInstance()).thenReturn(downloadProxy);
//構(gòu)建List<DownloadInfo>
ArrayList<DownloadInfo> downloadInfos = new ArrayList<>();
DownloadInfo downloadInfo = new DownloadInfo();
downloadInfos.add(downloadInfo);
//返回設(shè)置為構(gòu)建的list
PowerMockito.when(downloadProxy.getDownloadInfoList(Mockito.any(SimpleDownloadInfo.DownloadType.class),Mockito.anyBoolean())).thenReturn(downloadInfos);

私有方法返回修改值

public class MockPrivateObjectClass {
    public String stepName;
    public MockPrivateObjectClass() {
    }

    private void setStepName() {
        System.out.print("enter setStepName");
        stepName = "has set name";
    }

    public void testStepName() {
        setStepName();
        System.out.print(stepName);
    }
}

  @Test
    public void replacePrivateMethodTest() {
        MockPrivateObjectClass objectClass = new MockPrivateObjectClass();
        PowerMockito.replace(PowerMockito.method(MockPrivateObjectClass.class, "setStepName")).with(new InvocationHandler() {
            @Override
            public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
                Whitebox.setInternalState(o, "stepName", "modify step name");
                return null;
            }
        });
        objectClass.testStepName();
    }

invoke對(duì)象私有方法

ObjectWhiteBoxClass objectWhiteBoxClass = new ObjectWhiteBoxClass();
Whitebox.invokeMethod(objectWhiteBoxClass, “addObject", "test1");
Assert.assertEquals(1, objectWhiteBoxClass.getObjectList().size());
3.3 驗(yàn)證行為
驗(yàn)證返回值:

Assert.assertFalse、Assert.assertEquals等

Assert.assertEquals("0", mockAppManagerModule.getPkgScanStatus());
Assert.assertFalse(appManagerModule.isIgnoreApp("22"););
驗(yàn)證方法被調(diào)用及其頻率:(要求對(duì)象是mock對(duì)象)

Mockito.verify、PowerMotiko.verifyStatic、Mockito.verifyPrivate等

public/protected/default方法:

NormalClassB b = new MockNormalClassB();
 NormalClassB mockB = Mockito.spy(b);
Mockito.verify(mockB).getName();

Mockito.verify(triggerManager, Mockito.times(1)).showDesktopWindowLocked(
        Mockito.any(Context.class), Mockito.any(DesktopWinTrigger.class),
        Mockito.any(DesktopWinCardInfo.class),
        Mockito.anyLong(), Mockito.anyInt());

static方法:

PowerMockito.verifyStatic(Mockito.times(1));
ToastUtils.show(null, 0x7632343, Toast.LENGTH_SHORT);

private方法:

NormalClassA classA = new NormalClassA(); 
NormalClassA mockClassA = PowerMockito.spy(classA);
PowerMockito.verifyPrivate(mockClassA, times(1)).invoke("privateAdd", Mockito.anyInt(), Mockito.anyInt());

四、常見報(bào)錯(cuò)問題處理

持續(xù)更新中…..

?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過簡(jiǎn)信或評(píng)論聯(lián)系作者。

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

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