Robolectric使用教程

概述

Android的單元測試可以分為兩部分:

  1. Local unit tests:運(yùn)行于本地JVM
  2. Instrumented test:運(yùn)行于真機(jī)或者模擬器

如果使用Local測試,需要保證測試過程中不會(huì)調(diào)用Android系統(tǒng)API媚送,否則會(huì)拋出RuntimeException異常,因?yàn)長ocal測試是直接跑在本機(jī)JVM的,而之所以我們能使用Android系統(tǒng)API掰曾,是因?yàn)榫幾g的時(shí)候茧痕,我們依賴了一個(gè)名為“android.jar”的jar包财岔,但是jar包里所有方法都是直接拋出了一個(gè)RuntimeException,是沒有任何任何實(shí)現(xiàn)的盏浇,這只是Android為了我們能通過編譯提供的一個(gè)Stub!當(dāng)APP運(yùn)行在真實(shí)的Android系統(tǒng)的時(shí)候芽狗,由于類加載機(jī)制绢掰,會(huì)加載位于framework的具有真正實(shí)現(xiàn)的類。由于我們的Local是直接在PC上運(yùn)行的,所以調(diào)用這些系統(tǒng)API便會(huì)出錯(cuò)滴劲。
那么問題來了攻晒,我們既要使用Local測試,但測試過程又難免遇到調(diào)用系統(tǒng)API那怎么辦哑芹?其中一個(gè)方法就是mock objects炎辨,比如借助Mockito,另外一種方式就是使用Robolectric聪姿, Robolectric就是為解決這個(gè)問題而生的碴萧。它實(shí)現(xiàn)一套JVM能運(yùn)行的Android代碼,然后在unit test運(yùn)行的時(shí)候去截取android相關(guān)的代碼調(diào)用末购,然后轉(zhuǎn)到他們的他們實(shí)現(xiàn)的Shadow代碼去執(zhí)行這個(gè)調(diào)用的過程

如何使用破喻?

為項(xiàng)目添加依賴

testCompile "org.robolectric:robolectric:3.1.4"

Robolectric在第一次運(yùn)行時(shí),會(huì)下載一些sdk依賴包盟榴,每個(gè)sdk依賴包大概50M曹质,下載速度比較慢,用戶可以直接在網(wǎng)上下載相應(yīng)依賴包擎场,放置在本地maven倉庫地址中羽德,默認(rèn)路徑為:C:\Users\username\.m2\repository\org\robolectric

指定RobolectricTestRunner為運(yùn)行器

為測試用例添加注解,指定測試運(yùn)行器為RobolectricTestRunner。注意迅办,這里要通過Config指定constants = BuildConfig.class宅静,Robolectric 會(huì)通過constants推導(dǎo)出輸出路徑,如果不進(jìn)行配置站欺,Robolectric可能不能找到你的manifest姨夹、resources和assets資源

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class)
public class MainActivityTest {

}

什么是Shadow類

Shadow是Robolectric的立足之本,如其名矾策,作為影子磷账,一定是變幻莫測,時(shí)有時(shí)無贾虽,且依存于本尊逃糟。Robolectric定義了大量模擬Android系統(tǒng)類行為的Shadow類,當(dāng)這些系統(tǒng)類被創(chuàng)建的時(shí)候蓬豁,Robolectric會(huì)查找對應(yīng)的Shadow類并創(chuàng)建一個(gè)Shadow類與原始類關(guān)聯(lián)履磨。每當(dāng)系統(tǒng)類的方法被調(diào)用的時(shí)候,Robolectric會(huì)保證Shadow對應(yīng)的方法會(huì)調(diào)用庆尘。這些Shadow對象剃诅,豐富了本尊的行為,能更方便的對Android相關(guān)的對象進(jìn)行測試驶忌。
比如矛辕,我們可以借助ShadowActivity驗(yàn)證頁面是否正確跳轉(zhuǎn)了

    /**
     * 驗(yàn)證點(diǎn)擊事件是否觸發(fā)了頁面跳轉(zhuǎn)笑跛,驗(yàn)證目標(biāo)頁面是否預(yù)期頁面
     *
     * @throws Exception
     */
    @Test
    public void testJump() throws Exception {
        // 默認(rèn)會(huì)調(diào)用Activity的生命周期: onCreate->onStart->onResume
        MainActivity activity = Robolectric.setupActivity(MainActivity.class);
        // 觸發(fā)按鈕點(diǎn)擊
        activity.findViewById(R.id.activity_main_jump).performClick();

        // 獲取對應(yīng)的Shadow類
        ShadowActivity shadowActivity = Shadows.shadowOf(activity);
        // 借助Shadow類獲取啟動(dòng)下一Activity的Intent
        Intent nextIntent = shadowActivity.getNextStartedActivity();
        // 校驗(yàn)Intent的正確性
        assertEquals(nextIntent.getComponent().getClassName(), SecondActivity.class.getName());
    }

@Config配置

可以通過@Config定制Robolectric的運(yùn)行時(shí)的行為。這個(gè)注解可以用來注釋類和方法聊品,如果類和方法同時(shí)使用了@Config飞蹂,那么方法的設(shè)置會(huì)覆蓋類的設(shè)置。你可以創(chuàng)建一個(gè)基類翻屈,用@Config配置測試參數(shù)陈哑,這樣,其他測試用例就可以共享這個(gè)配置了

配置SDK版本

Robolectric會(huì)根據(jù)manifest文件配置的targetSdkVersion選擇運(yùn)行測試代碼的SDK版本伸眶,如果你想指定sdk來運(yùn)行測試用例惊窖,可以通過下面的方式配置

@Config(sdk = Build.VERSION_CODES.JELLY_BEAN)
public class SandwichTest {

    @Config(sdk = Build.VERSION_CODES.KITKAT)
    public void getSandwich_shouldReturnHamSandwich() {
    }
}

配置Application類

Robolectric會(huì)根據(jù)manifest文件配置的Application配置去實(shí)例化一個(gè)Application類,如果你想在測試用例中重新指定厘贼,可以通過下面的方式配置

@Config(application = CustomApplication.class)
public class SandwichTest {

    @Config(application = CustomApplicationOverride.class)
    public void getSandwich_shouldReturnHamSandwich() {
    }
}

指定Resource路徑

Robolectric可以讓你配置manifest界酒、resource和assets路徑,可以通過下面的方式配置

@Config(manifest = "some/build/path/AndroidManifest.xml",
        assetDir = "some/build/path/assetDir",
        resourceDir = "some/build/path/resourceDir")
public class SandwichTest {

    @Config(manifest = "other/build/path/AndroidManifest.xml")
    public void getSandwich_shouldReturnHamSandwich() {
    }
}

使用第三方Library Resources

當(dāng)Robolectric測試的時(shí)候嘴秸,會(huì)嘗試加載所有應(yīng)用提供的資源毁欣,但如果你需要使用第三方庫中提供的資源文件,你可能需要做一些特別的配置岳掐。不過如果你使用gradle來構(gòu)建Android應(yīng)用凭疮,這些配置就不需要做了,因?yàn)镚radle Plugin會(huì)在build的時(shí)候自動(dòng)合并第三方庫的資源串述,但如果你使用的是Maven哭尝,那么你需要配置libraries變量:

@RunWith(RobolectricTestRunner.class)
@Config(libraries = {
    "build/unpacked-libraries/library1",
    "build/unpacked-libraries/library2"
})
public class SandwichTest {
}

使用限定的資源文件

Android會(huì)在運(yùn)行時(shí)加載特定的資源文件,如根據(jù)設(shè)備屏幕加載不同分辨率的圖片資源剖煌、根據(jù)系統(tǒng)語言加載不同的string.xml,在Robolectric測試當(dāng)中逝淹,你也可以進(jìn)行一個(gè)限定耕姊,讓測試程序加載特定資源.多個(gè)限定條件可以用破折號拼接在在一起。

    /**
     * 使用qualifiers加載對應(yīng)的資源文件
     *
     * @throws Exception
     */
    @Config(qualifiers = "zh-rCN")
    @Test
    public void testString() throws Exception {
        final Context context = RuntimeEnvironment.application;
        assertThat(context.getString(R.string.app_name), is("單元測試Demo"));
    }

Properties文件

如果你嫌通過注解配置上面的東西麻煩栅葡,你也可以把以上配置放在一個(gè)Properties文件之中茉兰,然后通過@Config指定配置文件,比如欣簇,首先創(chuàng)建一個(gè)配置文件robolectric.properties:

# 放置Robolectric的配置選項(xiàng):
sdk=21
manifest=some/build/path/AndroidManifest.xml
assetDir=some/build/path/assetDir
resourceDir=some/build/path/resourceDir

然后把robolectric.properties文件放到src/test/resources目錄下规脸,運(yùn)行的時(shí)候,會(huì)自動(dòng)加載里面的配置

系統(tǒng)屬性配置

  • robolectric.offline:true代表關(guān)閉運(yùn)行時(shí)獲取jar包
  • robolectric.dependency.dir:當(dāng)處于offline模式的時(shí)候熊咽,指定運(yùn)行時(shí)的依賴目錄
  • robolectric.dependency.repo.id:設(shè)置運(yùn)行時(shí)獲取依賴的Maven倉庫ID莫鸭,默認(rèn)是sonatype
  • robolectric.dependency.repo.url:設(shè)置運(yùn)行時(shí)依賴的Maven倉庫地址,默認(rèn)是https://oss.sonatype.org/content/groups/public/
  • robolectric.logging.enabled:設(shè)置是否打開調(diào)試開關(guān)

以上設(shè)置可以通過Gradle進(jìn)行配置横殴,如:

android {

    testOptions {
        unitTests.all {
            systemProperty 'robolectric.dependency.repo.url', 'https://local-mirror/repo'
            systemProperty 'robolectric.dependency.repo.id', 'local'
        }
    }
}

驅(qū)動(dòng)Activity生命周期

利用ActivityController我們可以讓Activity執(zhí)行相應(yīng)的生命周期方法被因,如:

    @Test
    public void testLifecycle() throws Exception {
        // 創(chuàng)建Activity控制器
        ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class);
        MainActivity activity = controller.get();
        assertNull(activity.getLifecycleState());

        // 調(diào)用Activity的performCreate方法
        controller.create();
        assertEquals("onCreate", activity.getLifecycleState());

        // 調(diào)用Activity的performStart方法
        controller.start();
        assertEquals("onStart", activity.getLifecycleState());

        // 調(diào)用Activity的performResume方法
        controller.resume();
        assertEquals("onResume", activity.getLifecycleState());

        // 調(diào)用Activity的performPause方法
        controller.pause();
        assertEquals("onPause", activity.getLifecycleState());

        // 調(diào)用Activity的performStop方法
        controller.stop();
        assertEquals("onStop", activity.getLifecycleState());

        // 調(diào)用Activity的performRestart方法
        controller.restart();
        // 注意此處應(yīng)該是onStart,因?yàn)閜erformRestart不僅會(huì)調(diào)用restart,還會(huì)調(diào)用onStart
        assertEquals("onStart", activity.getLifecycleState());

        // 調(diào)用Activity的performDestroy方法
        controller.destroy();
        assertEquals("onDestroy", activity.getLifecycleState());
    }

通過ActivityController梨与,我們可以模擬各種生命周期的變化堕花。但是要注意,我們雖然可以隨意調(diào)用Activity的生命周期粥鞋,但是Activity生命周期切換有自己的檢測機(jī)制缘挽,我們要遵循Activity的生命周期規(guī)律。比如呻粹,如果當(dāng)前Activity并非處于stop狀態(tài)壕曼,測試代碼去調(diào)用了controller.restart方法,此時(shí)Activity是不會(huì)回調(diào)onRestart和onStart的尚猿。

除了控制生命周期窝稿,還可以在啟動(dòng)Activity的時(shí)候傳遞Intent:

    /**
     * 啟動(dòng)Activity的時(shí)候傳遞Intent
     *
     * @throws Exception
     */
    @Test
    public void testStartActivityWithIntent() throws Exception {
        Intent intent = new Intent();
        intent.putExtra("test", "HelloWorld");
        Activity activity = Robolectric.buildActivity(MainActivity.class).withIntent(intent).create().get();
        assertEquals("HelloWorld", activity.getIntent().getExtras().getString("test"));
    }

onRestoreInstanceState回調(diào)中傳遞Bundle:

    /**
     * savedInstanceState會(huì)在onRestoreInstanceState回調(diào)中傳遞給Activity
     *
     * @throws Exception
     */
    @Test
    public void testSavedInstanceState() throws Exception {
        Bundle savedInstanceState = new Bundle();
        Robolectric.buildActivity(MainActivity.class).create().restoreInstanceState(savedInstanceState).get();
        // verify something
    }

在真實(shí)環(huán)境下,視圖是在onCreate之后的某一時(shí)刻在attach到Window上的凿掂,在此之前伴榔,View是處于不可操作狀態(tài)的,你不能點(diǎn)擊它庄萎。在Activity的onPostResume方法調(diào)用之后踪少,View才會(huì)attach到Window之中。但是糠涛,在Robolectric之中援奢,我們可以用控制器的visible方法使得View變?yōu)榭梢姡優(yōu)榭梢娭笕碳瘢涂梢阅M點(diǎn)擊事件了

    @Test
    public void testVisible() throws Exception {
        ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class);
        MainActivity activity = controller.get();

        // 調(diào)用Activity的performCreate并且設(shè)置視圖visible
        controller.create().visible();
        // 觸發(fā)點(diǎn)擊
        activity.findViewById(R.id.activity_main_button1).performClick();

        // 驗(yàn)證
        assertEquals(shadowOf(activity).getNextStartedActivity().getComponent().getClassName(), SecondActivity.class.getName());
    }

追加模塊

為了減少依賴包的大小集漾,Robolectric的shadows類成了好幾部分:

SDK Package Robolectric Add-On Package
com.android.support.support-v4 org.robolectric:shadows-support-v4
com.android.support.multidex org.robolectric:shadows-multidex
com.google.android.gms:play-services org.robolectric:shadows-play-services
com.google.android.maps:maps org.robolectric:shadows-maps
org.apache.httpcomponents:httpclient org.robolectric:shadows-httpclient

用戶可以根據(jù)自身需求添加以下依賴包,如

dependencies {
    ... ...
    testCompile 'org.robolectric:robolectric:3.1.4'
    testCompile 'org.robolectric:shadows-support-v4:3.1.4'
    testCompile 'org.robolectric:shadows-multidex:3.1.4'
    testCompile 'org.robolectric:shadows-play-services:3.1.4'
    testCompile 'org.robolectric:shadows-maps:3.1.4'
    testCompile 'org.robolectric:shadows-httpclient:3.1.4'
}

自定義Shadow類

  1. Shadow類需要一個(gè)public的無參構(gòu)造方法以方便Robolectric框架可以實(shí)例化它砸脊,通過@Implements注解與原始類關(guān)聯(lián)在一起
  2. 若原始類有有參構(gòu)造方法具篇,在Shadow類中定義public void類型的名為__constructor__的方法,且方法參數(shù)與原始類的構(gòu)造方法參數(shù)一直
  3. 定義與原始類方法簽名一致的方法凌埂,在里面重寫實(shí)現(xiàn)驱显,Shadow方法需用@Implementation進(jìn)行注解

下面我們來創(chuàng)建RobolectricBean的Shadow類
原始類:

public class RobolectricBean {

    String name;
    int    color;

    public RobolectricBean(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public int getColor() {
        return color;
    }

    public void setColor(int color) {
        this.color = color;
    }
}

Shadow類:

/**
 * 創(chuàng)建{@link RobolectricBean}的影子類
 *
 * @author HansChen
 */
@Implements(RobolectricBean.class)
public class ShadowRobolectricBean {

    /**
     * 通過@RealObject注解可以訪問原始對象,但注意瞳抓,通過@RealObject注解的變量調(diào)用方法埃疫,依然會(huì)調(diào)用Shadow類的方法,而不是原始類的方法
     * 只能用來訪問原始類的field
     */
    @RealObject
    RobolectricBean realBean;

    /**
     * 需要一個(gè)無參構(gòu)造方法
     */
    public ShadowRobolectricBean() {

    }

    /**
     * 對應(yīng)原始類的構(gòu)造方法
     *
     * @param name 對應(yīng)原始類構(gòu)造方法的傳入?yún)?shù)
     */
    public void __constructor__(String name) {
        realBean.name = name;
    }

    /**
     * 原始對象的方法被調(diào)用的時(shí)候孩哑,Robolectric會(huì)根據(jù)方法簽名查找對應(yīng)的Shadow方法并調(diào)用
     */
    @Implementation
    public String getName() {
        return "Hello, I ma shadow of RobolectricBean: " + realBean.name;
    }

    @Implementation
    public int getColor() {
        return realBean.color;
    }

    @Implementation
    public void setColor(int color) {
        realBean.color = color;
    }
}

Shadow類中訪問原始類的field

Shadow類中可以定義一個(gè)原始類的成員變量栓霜,并用@RealObject注解,這樣横蜒,Shadow類就能訪問原始類的field了叙淌,但是注意秤掌,通過@RealObject注解的變量調(diào)用方法,依然會(huì)調(diào)用Shadow類的方法鹰霍,而不是原始類的方法闻鉴,只能用它來訪問原始類的field。

@Implements(Point.class)
public class ShadowPoint {
    @RealObject private Point realPoint;
    ...
    public void __constructor__(int x, int y) {
        realPoint.x = x;
        realPoint.y = y;
    }
}

如何在測試用例中讓Shadow生效

在Config注解中添加shadows參數(shù)茂洒,指定對應(yīng)的Shadow生效

@RunWith(RobolectricTestRunner.class)
@Config(shadows = ShadowRobolectricBean.class)
public class RobolectricBeanTest {

    ... ...
}

注意孟岛,自定義的Shadow類不能通過Shadows.shadowOf()獲取,需要用ShadowExtractor.extract()來獲取督勺,獲取之后進(jìn)行類型轉(zhuǎn)換:

ShadowRobolectricBean shadowBean = (ShadowRobolectricBean) ShadowExtractor.extract(bean);

常用測試場景

頁面跳轉(zhuǎn)驗(yàn)證

    /**
     * 驗(yàn)證點(diǎn)擊事件是否觸發(fā)了頁面跳轉(zhuǎn)渠羞,驗(yàn)證目標(biāo)頁面是否預(yù)期頁面
     *
     * @throws Exception
     */
    @Test
    public void testJump() throws Exception {
        // 默認(rèn)會(huì)調(diào)用Activity的生命周期: onCreate->onStart->onResume
        MainActivity activity = Robolectric.setupActivity(MainActivity.class);
        // 觸發(fā)按鈕點(diǎn)擊
        activity.findViewById(R.id.activity_main_jump).performClick();

        // 獲取對應(yīng)的Shadow類
        ShadowActivity shadowActivity = Shadows.shadowOf(activity);
        // 借助Shadow類獲取啟動(dòng)下一Activity的Intent
        Intent nextIntent = shadowActivity.getNextStartedActivity();
        // 校驗(yàn)Intent的正確性
        assertEquals(nextIntent.getComponent().getClassName(), SecondActivity.class.getName());
    }

UI組件狀態(tài)驗(yàn)證

    /**
     * 驗(yàn)證UI組件狀態(tài)
     *
     * @throws Exception
     */
    @Test
    public void testCheckBoxState() throws Exception {
        MainActivity activity = Robolectric.setupActivity(MainActivity.class);
        CheckBox checkBox = (CheckBox) activity.findViewById(R.id.activity_main_check_box);
        // 驗(yàn)證CheckBox初始狀態(tài)
        assertFalse(checkBox.isChecked());

        // 點(diǎn)擊按鈕反轉(zhuǎn)CheckBox狀態(tài)
        activity.findViewById(R.id.activity_main_switch_check_box).performClick();
        // 驗(yàn)證狀態(tài)是否正確
        assertTrue(checkBox.isChecked());

        // 點(diǎn)擊按鈕反轉(zhuǎn)CheckBox狀態(tài)
        activity.findViewById(R.id.activity_main_switch_check_box).performClick();
        // 驗(yàn)證狀態(tài)是否正確
        assertFalse(checkBox.isChecked());
    }

驗(yàn)證Dialog

    /**
     * 驗(yàn)證Dialog是否正確彈出
     *
     * @throws Exception
     */
    @Test
    public void testDialog() throws Exception {
        MainActivity activity = Robolectric.setupActivity(MainActivity.class);
        AlertDialog dialog = ShadowAlertDialog.getLatestAlertDialog();
        // 判斷Dialog尚未彈出
        assertNull(dialog);

        activity.findViewById(R.id.activity_main_show_dialog).performClick();
        dialog = ShadowAlertDialog.getLatestAlertDialog();
        // 判斷Dialog已經(jīng)彈出
        assertNotNull(dialog);
        // 獲取Shadow類進(jìn)行驗(yàn)證
        ShadowAlertDialog shadowDialog = shadowOf(dialog);
        assertEquals("AlertDialog", shadowDialog.getTitle());
        assertEquals("Oops, now you see me ~", shadowDialog.getMessage());
    }

驗(yàn)證Toast

    /**
     * 驗(yàn)證Toast是否正確彈出
     *
     * @throws Exception
     */
    @Test
    public void testToast() throws Exception {
        MainActivity activity = Robolectric.setupActivity(MainActivity.class);
        Toast toast = ShadowToast.getLatestToast();
        // 判斷Toast尚未彈出
        assertNull(toast);

        activity.findViewById(R.id.activity_main_show_toast).performClick();
        toast = ShadowToast.getLatestToast();
        // 判斷Toast已經(jīng)彈出
        assertNotNull(toast);
        // 獲取Shadow類進(jìn)行驗(yàn)證
        ShadowToast shadowToast = shadowOf(toast);
        assertEquals(Toast.LENGTH_SHORT, shadowToast.getDuration());
        assertEquals("oops", ShadowToast.getTextOfLatestToast());
    }

驗(yàn)證Fragment

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, application = CustomApplication.class)
public class MyFragmentTest {

    private MyFragment myFragment;

    @Before
    public void setUp() throws Exception {
        myFragment = new MyFragment();
        // 把Fragment添加到Activity中
        FragmentTestUtil.startFragment(myFragment);
    }

    @Test
    public void testFragment() throws Exception {
        assertNotNull(myFragment.getView());
    }
}

驗(yàn)證BroadcastReceiver

首先看下廣播接收器:

public class MyReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        // do something
    }
}

廣播的測試點(diǎn)可以包含兩個(gè)方面

  1. 驗(yàn)證應(yīng)用程序是否注冊了該廣播
  2. 驗(yàn)證廣播接收器的處理邏輯是否正確,關(guān)于邏輯是否正確智哀,可以直接人為的觸發(fā)onReceive()方法次询,讓然后進(jìn)行驗(yàn)證
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, application = CustomApplication.class)
public class MyReceiverTest {


    @Test
    public void restRegister() throws Exception {
        ShadowApplication shadowApplication = ShadowApplication.getInstance();

        String action = "ut.cn.unittestdemo.receiver";
        Intent intent = new Intent(action);

        // 驗(yàn)證是否注冊了相應(yīng)的Receiver
        assertTrue(shadowApplication.hasReceiverForIntent(intent));
    }

    @Test
    public void restReceive() throws Exception {

        String action = "ut.cn.unittestdemo.receiver";
        Intent intent = new Intent(action);
        intent.putExtra("EXTRA_USERNAME", "HansChen");

        MyReceiver myReceiver = new MyReceiver();
        myReceiver.onReceive(RuntimeEnvironment.application, intent);
        // verify something
    }
}

驗(yàn)證Service

Service和Activity一樣,都有生命周期瓷叫,Robolectric也提供了Service的生命周期控制器屯吊,使用方式和Activity類似,這里就不做詳細(xì)解釋了

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, application = CustomApplication.class)
public class TestServiceTest {

    private ServiceController<TestService> controller;
    private TestService                    testService;

    @Before
    public void setUp() throws Exception {
        controller = Robolectric.buildService(TestService.class);
        testService = controller.get();
    }

    /**
     * 控制Service生命周期進(jìn)行驗(yàn)證
     *
     * @throws Exception
     */
    @Test
    public void testLifecycle() throws Exception {

        controller.create();
        // verify something

        controller.startCommand(0, 0);
        // verify something

        controller.bind();
        // verify something

        controller.unbind();
        // verify something

        controller.destroy();
        // verify something
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末摹菠,一起剝皮案震驚了整個(gè)濱河市盒卸,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌次氨,老刑警劉巖蔽介,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異煮寡,居然都是意外死亡虹蓄,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進(jìn)店門幸撕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來薇组,“玉大人,你說我怎么就攤上這事杈帐。” “怎么了专钉?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵挑童,是天一觀的道長。 經(jīng)常有香客問我跃须,道長站叼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任菇民,我火速辦了婚禮尽楔,結(jié)果婚禮上投储,老公的妹妹穿的比我還像新娘。我一直安慰自己阔馋,他們只是感情好玛荞,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著呕寝,像睡著了一般勋眯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上下梢,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天客蹋,我揣著相機(jī)與錄音,去河邊找鬼孽江。 笑死讶坯,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的岗屏。 我是一名探鬼主播辆琅,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼担汤!你這毒婦竟也來了涎跨?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤崭歧,失蹤者是張志新(化名)和其女友劉穎隅很,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體率碾,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡叔营,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了所宰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绒尊。...
    茶點(diǎn)故事閱讀 38,654評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖仔粥,靈堂內(nèi)的尸體忽然破棺而出婴谱,到底是詐尸還是另有隱情,我是刑警寧澤躯泰,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布谭羔,位于F島的核電站,受9級特大地震影響麦向,放射性物質(zhì)發(fā)生泄漏瘟裸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一诵竭、第九天 我趴在偏房一處隱蔽的房頂上張望话告。 院中可真熱鬧兼搏,春花似錦、人聲如沸沙郭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽棠绘。三九已至件相,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間氧苍,已是汗流浹背夜矗。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留让虐,地道東北人紊撕。 一個(gè)月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像赡突,于是被迫代替她去往敵國和親对扶。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評論 2 349

推薦閱讀更多精彩內(nèi)容

  • Android單元測試介紹 處于高速迭代開發(fā)中的Android項(xiàng)目往往需要除黑盒測試外更加可靠的質(zhì)量保障惭缰,這正是單...
    東經(jīng)315度閱讀 3,096評論 6 37
  • Extending Robolectric Shadow Classes Robolectric 定義了很多sha...
    null_null閱讀 3,823評論 1 1
  • 一.基本介紹 背景: 目前處于高速迭代開發(fā)中的Android項(xiàng)目往往需要除黑盒測試外更加可靠的質(zhì)量保障浪南,這正是單元...
    anmi7閱讀 2,019評論 0 6
  • 標(biāo)簽(空格分隔): Android 單元測試的好處:Martin Fowler在《重構(gòu)》里面還解釋了為什么單元測試...
    背影殺手不太冷閱讀 5,816評論 3 25
  • ¥開啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個(gè)線程,因...
    小菜c閱讀 6,367評論 0 17