Android單元測(cè)試框架Robolectric3.0介紹(一)

一、關(guān)于Robolectric3.0

文章中的所有代碼在此:https://github.com/geniusmart/LoveUT 欺缘,由于 Robolectric 3.0 和 3.1 版本(包括后續(xù)3.x版本)差異不小栋豫,該工程中包含這兩個(gè)版本對(duì)應(yīng)的測(cè)試用例 Demo 。

作為一個(gè)軟件開(kāi)發(fā)攻城獅谚殊,無(wú)論你多不屑多排斥單元測(cè)試丧鸯,它都是一種非常好的開(kāi)發(fā)方式,且不談TDD嫩絮,為自己寫的代碼負(fù)責(zé)丛肢,測(cè)試自己寫的代碼围肥,在自己力所能及的范圍內(nèi)提高產(chǎn)品的質(zhì)量,本是理所當(dāng)然的事情蜂怎。

那么如何測(cè)試自己寫的代碼穆刻?點(diǎn)點(diǎn)界面,測(cè)測(cè)功能固然是一種方式杠步,但是如果能留下一段一勞永逸的測(cè)試代碼蛹批,讓代碼測(cè)試代碼,豈不兩全其美篮愉?所以,寫好單元測(cè)試差导,愛(ài)惜自己的代碼试躏,愛(ài)惜顏值高的QA妹紙,愛(ài)惜有價(jià)值的產(chǎn)品(沒(méi)價(jià)值的设褐、政治性的颠蕴、屁股決定腦袋的產(chǎn)品滾粗),人人有責(zé)助析!

對(duì)于Android app來(lái)說(shuō)犀被,寫起單元測(cè)試來(lái)瞻前顧后,一方面單元測(cè)試需要運(yùn)行在模擬器上或者真機(jī)上外冀,麻煩而且緩慢寡键,另一方面,一些依賴Android SDK的對(duì)象(如Activity雪隧,TextView等)的測(cè)試非常頭疼西轩,Robolectric可以解決此類問(wèn)題,它的設(shè)計(jì)思路便是通過(guò)實(shí)現(xiàn)一套JVM能運(yùn)行的Android代碼脑沿,從而做到脫離Android環(huán)境進(jìn)行測(cè)試藕畔。本文對(duì)Robolectric3.0做了簡(jiǎn)單介紹,并列舉了如何對(duì)Android的組件和常見(jiàn)功能進(jìn)行測(cè)試的示例庄拇。

二注服、環(huán)境搭建

Gradle配置

在build.gradle中配置如下依賴關(guān)系:

testCompile "org.robolectric:robolectric:3.0"

通過(guò)注解配置TestRunner

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class SampleActivityTest {

}

Android Studio的配置

  1. 在Build Variants面板中,將Test Artifact切換成Unit Tests模式(注:新版本的as已經(jīng)不需要做這項(xiàng)配置)措近,如下圖:


    配置Test Artifact
  2. working directory 設(shè)置為$MODULE_DIR$

如果在測(cè)試過(guò)程遇見(jiàn)如下問(wèn)題溶弟,解決的方式就是設(shè)置working directory的值:

java.io.FileNotFoundException: build\intermediates\bundles\debug\AndroidManifest.xml (系統(tǒng)找不到指定的路徑。)

設(shè)置方法如下圖所示:

Edit Configurations
Working directory的配置

更多環(huán)境配置可以參考官方網(wǎng)站瞭郑。

三可很、Activity的測(cè)試

  1. 創(chuàng)建Activity實(shí)例
@Test
public void testActivity() {
        SampleActivity sampleActivity = Robolectric.setupActivity(SampleActivity.class);
        assertNotNull(sampleActivity);
        assertEquals(sampleActivity.getTitle(), "SimpleActivity");
    }
  1. 生命周期
@Test
public void testLifecycle() {
        ActivityController<SampleActivity> activityController = Robolectric.buildActivity(SampleActivity.class).create().start();
        Activity activity = activityController.get();
        TextView textview = (TextView) activity.findViewById(R.id.tv_lifecycle_value);
        assertEquals("onCreate",textview.getText().toString());
        activityController.resume();
        assertEquals("onResume", textview.getText().toString());
        activityController.destroy();
        assertEquals("onDestroy", textview.getText().toString());
    }
  1. 跳轉(zhuǎn)
@Test
public void testStartActivity() {
        //按鈕點(diǎn)擊后跳轉(zhuǎn)到下一個(gè)Activity
        forwardBtn.performClick();
        Intent expectedIntent = new Intent(sampleActivity, LoginActivity.class);
        Intent actualIntent = ShadowApplication.getInstance().getNextStartedActivity();
        assertEquals(expectedIntent, actualIntent);
    }

注:Robolectric 3.1 之后,不建議用 Intent.equals() 的方式來(lái)比對(duì)兩個(gè) Intent 凰浮,因此以上代碼將無(wú)法正常執(zhí)行我抠。目前建議用類似代碼來(lái)斷言:

assertEquals(expectedIntent.getComponent(), actualIntent.getComponent());

當(dāng)然苇本,Intent 有很多屬性,如果需要分別斷言的話比較麻煩菜拓,因此可以用一些第三方庫(kù)瓣窄,比如 assertj-android 的工具類 IntentAssert

  1. UI組件狀態(tài)
@Test
public void testViewState(){
        CheckBox checkBox = (CheckBox) sampleActivity.findViewById(R.id.checkbox);
        Button inverseBtn = (Button) sampleActivity.findViewById(R.id.btn_inverse);
        assertTrue(inverseBtn.isEnabled());

        checkBox.setChecked(true);
        //點(diǎn)擊按鈕纳鼎,CheckBox反選
        inverseBtn.performClick();
        assertTrue(!checkBox.isChecked());
        inverseBtn.performClick();
        assertTrue(checkBox.isChecked());
    }
  1. Dialog
@Test
public void testDialog(){
        //點(diǎn)擊按鈕俺夕,出現(xiàn)對(duì)話框
        dialogBtn.performClick();
        AlertDialog latestAlertDialog = ShadowAlertDialog.getLatestAlertDialog();
        assertNotNull(latestAlertDialog);
    }
  1. Toast
@Test
public void testToast(){
        //點(diǎn)擊按鈕,出現(xiàn)吐司
        toastBtn.performClick();
        assertEquals(ShadowToast.getTextOfLatestToast(),"we love UT");
    }
  1. Fragment的測(cè)試
    如果使用support的Fragment贱鄙,需添加以下依賴
testCompile "org.robolectric:shadows-support-v4:3.0"

shadow-support包提供了將Fragment主動(dòng)添加到Activity中的方法:SupportFragmentTestUtil.startFragment(),簡(jiǎn)易的測(cè)試代碼如下

@Test
public void testFragment(){
    SampleFragment sampleFragment = new SampleFragment();
    //此api可以主動(dòng)添加Fragment到Activity中劝贸,因此會(huì)觸發(fā)Fragment的onCreateView()
    SupportFragmentTestUtil.startFragment(sampleFragment);
    assertNotNull(sampleFragment.getView());
}
  1. 訪問(wèn)資源文件
@Test
public void testResources() {
        Application application = RuntimeEnvironment.application;
        String appName = application.getString(R.string.app_name);
        String activityTitle = application.getString(R.string.title_activity_simple);
        assertEquals("LoveUT", appName);
        assertEquals("SimpleActivity",activityTitle);
    }

四、BroadcastReceiver的測(cè)試

首先看下廣播接收者的代碼

public class MyReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        SharedPreferences.Editor editor = context.getSharedPreferences(
                "account", Context.MODE_PRIVATE).edit();
        String name = intent.getStringExtra("EXTRA_USERNAME");
        editor.putString("USERNAME", name);
        editor.apply();
    }
}

廣播的測(cè)試點(diǎn)可以包含兩個(gè)方面逗宁,一是應(yīng)用程序是否注冊(cè)了該廣播映九,二是廣播接受者的處理邏輯是否正確,關(guān)于邏輯是否正確瞎颗,可以直接人為的觸發(fā)onReceive()方法件甥,驗(yàn)證執(zhí)行后所影響到的數(shù)據(jù)。

@Test
public void testBoradcast(){
        ShadowApplication shadowApplication = ShadowApplication.getInstance();

        String action = "com.geniusmart.loveut.login";
        Intent intent = new Intent(action);
        intent.putExtra("EXTRA_USERNAME", "geniusmart");

        //測(cè)試是否注冊(cè)廣播接收者
        assertTrue(shadowApplication.hasReceiverForIntent(intent));

        //以下測(cè)試廣播接受者的處理邏輯是否正確
        MyReceiver myReceiver = new MyReceiver();
        myReceiver.onReceive(RuntimeEnvironment.application,intent);
        SharedPreferences preferences = shadowApplication.getSharedPreferences("account", Context.MODE_PRIVATE);
        assertEquals( "geniusmart",preferences.getString("USERNAME", ""));
    }

五哼拔、Service的測(cè)試

Service的測(cè)試類似于BroadcastReceiver引有,以IntentService為例,可以直接觸發(fā)onHandleIntent()方法倦逐,用來(lái)驗(yàn)證Service啟動(dòng)后的邏輯是否正確譬正。

public class SampleIntentService extends IntentService {
    public SampleIntentService() {
        super("SampleIntentService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        SharedPreferences.Editor editor = getApplicationContext().getSharedPreferences(
                "example", Context.MODE_PRIVATE).edit();
        editor.putString("SAMPLE_DATA", "sample data");
        editor.apply();
    }
}

以上代碼的單元測(cè)試用例:

@Test
public void addsDataToSharedPreference() {
        Application application = RuntimeEnvironment.application;
        RoboSharedPreferences preferences = (RoboSharedPreferences) application
                .getSharedPreferences("example", Context.MODE_PRIVATE);

        SampleIntentService registrationService = new SampleIntentService();
        registrationService.onHandleIntent(new Intent());

        assertEquals(preferences.getString("SAMPLE_DATA", ""), "sample data");
    }

六、Shadow的使用

Shadow是Robolectric的立足之本檬姥,如其名导帝,作為影子客们,一定是變幻莫測(cè)钦购,時(shí)有時(shí)無(wú),且依存于本尊陌僵。因此荞雏,框架針對(duì)Android SDK中的對(duì)象虐秦,提供了很多影子對(duì)象(如Activity和ShadowActivity、TextView和ShadowTextView等)凤优,這些影子對(duì)象悦陋,豐富了本尊的行為,能更方便的對(duì)Android相關(guān)的對(duì)象進(jìn)行測(cè)試筑辨。

1.使用框架提供的Shadow對(duì)象

@Test
public void testDefaultShadow(){

    MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);

    //通過(guò)Shadows.shadowOf()可以獲取很多Android對(duì)象的Shadow對(duì)象
    ShadowActivity shadowActivity = Shadows.shadowOf(mainActivity);
    ShadowApplication shadowApplication = Shadows.shadowOf(RuntimeEnvironment.application);

    Bitmap bitmap = BitmapFactory.decodeFile("Path");
    ShadowBitmap shadowBitmap = Shadows.shadowOf(bitmap);

    //Shadow對(duì)象提供方便我們用于模擬業(yè)務(wù)場(chǎng)景進(jìn)行測(cè)試的api
    assertNull(shadowActivity.getNextStartedActivity());
    assertNull(shadowApplication.getNextStartedActivity());
    assertNotNull(shadowBitmap);

}   

2.如何自定義Shadow對(duì)象

首先俺驶,創(chuàng)建原始對(duì)象Person

public class Person {
    private String name;
    public Person(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}

其次,創(chuàng)建Person的Shadow對(duì)象

@Implements(Person.class)
public class ShadowPerson {

    @Implementation
    public String getName() {
        return "geniusmart";
    }
}

接下來(lái)棍辕,需自定義TestRunner暮现,添加Person對(duì)象為要進(jìn)行Shadow的對(duì)象(注:Robolectric 3.1 起可以省略此步驟)还绘。

public class CustomShadowTestRunner extends RobolectricGradleTestRunner {

    public CustomShadowTestRunner(Class<?> klass) throws InitializationError {
        super(klass);
    }

    @Override
    public InstrumentationConfiguration createClassLoaderConfig() {
        InstrumentationConfiguration.Builder builder = InstrumentationConfiguration.newBuilder();
        /**
         * 添加要進(jìn)行Shadow的對(duì)象
         */
        builder.addInstrumentedPackage(Person.class.getPackage().getName());
        builder.addInstrumentedClass(Person.class.getName());
        return builder.build();
    }
}

最后,在測(cè)試用例中栖袋,ShadowPerson對(duì)象將自動(dòng)代替原始對(duì)象拍顷,調(diào)用Shadow對(duì)象的數(shù)據(jù)和行為

@RunWith(CustomShadowTestRunner.class)
@Config(constants = BuildConfig.class,shadows = {ShadowPerson.class})
public class ShadowTest {

    /**
     * 測(cè)試自定義的Shadow
     */
    @Test
    public void testCustomShadow(){
        Person person = new Person("genius");
        //getName()實(shí)際上調(diào)用的是ShadowPerson的方法
        assertEquals("geniusmart", person.getName());

        //獲取Person對(duì)象對(duì)應(yīng)的Shadow對(duì)象
        ShadowPerson shadowPerson = (ShadowPerson) ShadowExtractor.extract(person);
        assertEquals("geniusmart", shadowPerson.getName());
    }
}

七、關(guān)于代碼

文章中的所有代碼在此:https://github.com/geniusmart/LoveUT
另外塘幅,除了文中所示的代碼之外昔案,該工程還包含了Robolectric官方的測(cè)試?yán)樱粋€(gè)簡(jiǎn)單的登錄功能的測(cè)試电媳,可以作為入門使用踏揣,界面如下圖。

官方的登錄測(cè)試DEMO

八匾乓、參考文章

http://robolectric.org
https://github.com/robolectric/robolectric
http://tech.meituan.com/Android_unit_test.html

關(guān)于代碼中的日志如何輸出捞稿、網(wǎng)絡(luò)請(qǐng)求、數(shù)據(jù)庫(kù)操作如何測(cè)試钝尸,請(qǐng)移步第二篇文章Android單元測(cè)試框架Robolectric3.0介紹(二)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市搂根,隨后出現(xiàn)的幾起案子珍促,更是在濱河造成了極大的恐慌,老刑警劉巖剩愧,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件猪叙,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡仁卷,警方通過(guò)查閱死者的電腦和手機(jī)穴翩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)锦积,“玉大人芒帕,你說(shuō)我怎么就攤上這事》峤椋” “怎么了背蟆?”我有些...
    開(kāi)封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)哮幢。 經(jīng)常有香客問(wèn)我带膀,道長(zhǎng),這世上最難降的妖魔是什么橙垢? 我笑而不...
    開(kāi)封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任垛叨,我火速辦了婚禮,結(jié)果婚禮上柜某,老公的妹妹穿的比我還像新娘嗽元。我一直安慰自己敛纲,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布还棱。 她就那樣靜靜地躺著载慈,像睡著了一般。 火紅的嫁衣襯著肌膚如雪珍手。 梳的紋絲不亂的頭發(fā)上办铡,一...
    開(kāi)封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音琳要,去河邊找鬼寡具。 笑死,一個(gè)胖子當(dāng)著我的面吹牛稚补,可吹牛的內(nèi)容都是我干的童叠。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼课幕,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼厦坛!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起乍惊,我...
    開(kāi)封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤杜秸,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后润绎,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體撬碟,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年莉撇,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了呢蛤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡棍郎,死狀恐怖其障,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情涂佃,我是刑警寧澤静秆,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站巡李,受9級(jí)特大地震影響抚笔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜侨拦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一殊橙、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦膨蛮、人聲如沸叠纹。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)誉察。三九已至,卻和暖如春惹谐,著一層夾襖步出監(jiān)牢的瞬間持偏,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工氨肌, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鸿秆,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓怎囚,卻偏偏與公主長(zhǎng)得像卿叽,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子恳守,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

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