單元測(cè)試框架:Robolectric

前言

前面我們介紹了單元測(cè)試框架 JUnitMockito 的使用(詳情查看:單元測(cè)試框架:JUnit單元測(cè)試框架:Mockito),對(duì)于絕大多數(shù)的 Java 方法,上面兩個(gè)框架的使用基本就能覆蓋絕大多數(shù)測(cè)試用例編寫(xiě)漆羔。然而,如果我們要對(duì) Android 代碼進(jìn)行測(cè)試忧陪,由于 Android 程序是跑在 Dalvik 虛擬機(jī)上的厘托,跟普通 Java 代碼跑在 JVM 上不同,因此流炕,無(wú)法直接在 JVM 上運(yùn)行 Android 程序澎现。

Why Robolectric

由于無(wú)法直接在 JVM 上運(yùn)行 Android 程序仅胞,因此平常我們對(duì) Android 應(yīng)用的測(cè)試都是通過(guò)直接將應(yīng)用部署到虛擬機(jī)/真機(jī)上進(jìn)行測(cè)試,而這個(gè)過(guò)程要經(jīng)過(guò)打包剑辫、dexing干旧、上傳到device、安裝妹蔽,運(yùn)行椎眯,打開(kāi)界面等一系列過(guò)程,十分浪費(fèi)時(shí)間胳岂,這對(duì)單元測(cè)試來(lái)說(shuō)是無(wú)法忍受的盅视。我們希望的 Android 單元測(cè)試是注重流程與實(shí)現(xiàn),易于測(cè)試旦万,時(shí)間快闹击,耗時(shí)短,我們不想經(jīng)歷 dexing成艘、打包赏半、部署 apk 到設(shè)備這些過(guò)程,我們不需要看見(jiàn)界面是否出現(xiàn)淆两,我們只想要測(cè)試相應(yīng)的代碼的邏輯與功能断箫,因此,Robolectric 應(yīng)運(yùn)而生秋冰。

Robolectric 是一套單元測(cè)試框架仲义,通過(guò) Robolectric,我們可以在 Java 虛擬機(jī)(JVM)上對(duì) Android 應(yīng)用進(jìn)行測(cè)試剑勾。

Robolectric 原理

Android 應(yīng)用是運(yùn)行在 Dalvik 虛擬機(jī)上的埃撵,而我們平常開(kāi)發(fā) Android app 是在 JVM 上的,因此Google 為我們開(kāi)發(fā) Android app 提供了 SDK(軟件開(kāi)發(fā)工具集)虽另,各個(gè) API-level 對(duì)應(yīng)的 SDK 都有相應(yīng)的 android.jar 包暂刘,通過(guò)這個(gè) androdi.jar 包,我們就可以在 JVM 上調(diào)用 Android 系統(tǒng) api捂刺,進(jìn)行 Android app 開(kāi)發(fā)谣拣。然而,這個(gè) android.jar 包的作用僅僅是起一個(gè)可以編譯打包的功能族展,我們是沒(méi)辦法直接在 JVM 上運(yùn)行 androdi.jar 的森缠,可以在 android_sdk_home/platforms/ 查看下 android.jar 包內(nèi)容,你會(huì)發(fā)現(xiàn)所有方法內(nèi)部只有一個(gè)實(shí)現(xiàn):throw RuntimeException("stub!!”);

因此仪缸,如果我們?cè)趩卧獪y(cè)試中直接運(yùn)行 Android 相關(guān)測(cè)試用例贵涵,那運(yùn)行的時(shí)候就會(huì)拋出 RuntimeException("stub!!”) 異常。從這里,我們也可以知道為什么這兩年 MVP,MVVM 這種框架流行的原因了独悴,一個(gè)原因就是為了隔離 Java 層代碼與 Android api 代碼的耦合例书,方便單元測(cè)試。

這里要注意的是刻炒,當(dāng)我們將寫(xiě)好的 Android 應(yīng)用部署到虛擬機(jī)/真機(jī)時(shí)决采,android.jar 就會(huì)被替換成系統(tǒng)真正的實(shí)現(xiàn),因此功能便能得到實(shí)現(xiàn)坟奥。

通過(guò)上面的討論树瞭,我們可以知道,無(wú)法在 JVM 上運(yùn)行 Android 代碼爱谁,是因?yàn)?JVM 上 Android api 接口內(nèi)部實(shí)現(xiàn)全部為throw RuntimeException("stub!!”);晒喷,因此,一個(gè)解決方法就是更改 android.jar 內(nèi)容访敌,真正實(shí)現(xiàn) Android api 接口凉敲。而 Robolectric 的實(shí)現(xiàn)原理正是如此,Robolectric 為我們實(shí)現(xiàn)了一套 JVM 能運(yùn)行的 Android api寺旺,而且是增強(qiáng)型的 Android api爷抓,其內(nèi)部比原生 Android api 增加了更多的方法,方便我們進(jìn)行調(diào)用測(cè)試阻塑。

Robolectric 優(yōu)點(diǎn)

  • 運(yùn)行 Android 單元測(cè)試蓝撇,無(wú)需啟動(dòng)虛擬機(jī)/真機(jī)
  • 復(fù)寫(xiě) Android 核心庫(kù)(即 影子類(lèi) - Shadow Classes),擴(kuò)展更多有用的功能陈莽。
  • 可以對(duì) Android 多個(gè)組件進(jìn)行測(cè)試渤昌,比如:
    -- Activity
    -- Service
    -- Broadcast Receiver
    可以對(duì)應(yīng)用資源進(jìn)行測(cè)試,比如:
    -- string.xml
    -- 應(yīng)用屬性配置(Configuration)走搁,比如橫屏或者豎屏
    -- Styles and themes
    可以進(jìn)行測(cè)試的還有:
    -- 多渠道(Multiple product flavors)

Integrate

via Gradle:

testImplementation "org.robolectric:robolectric:3.4.2"
//required  Android Studio 3.0 alpha 5
android {
  testOptions {
    unitTests {
      includeAndroidResources = true
    }
  }
} 

最新的版本可以在這里查找:Robolertic-newest-version

更多配置詳情独柑,請(qǐng)查看:Getting Started

如果使用的是 Android Studio,那么在運(yùn)行測(cè)試用例時(shí)如果出現(xiàn)錯(cuò)誤:

android.content.res.Resources$NotFoundException: String resource ID #0x7f0b001f

那么朱盐,還需進(jìn)行如下配置:
在 gradle.properties 中添加內(nèi)容:android.enableAapt2=false
For more detailed information,please see here

如果出現(xiàn)以下錯(cuò)誤:

No such manifest file: build\intermediates\bundles\debug\AndroidManifest.xml

那么群嗤,還需進(jìn)行如下配置:

Edit Configurations
Working directory

Sample

  • Activity
  1. Activity跳轉(zhuǎn)測(cè)試:摘自官方 Demo
    假設(shè)布局如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/login"
        android:text="Login"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</LinearLayout>

我們希望測(cè)試當(dāng)點(diǎn)擊按鈕時(shí),啟動(dòng)了LoginActivity

public class WelcomeActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.welcome_activity);

        final View button = findViewById(R.id.login);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                startActivity(new Intent(WelcomeActivity.this, LoginActivity.class));
            }
        });
    }
}

我們通過(guò)按鈕點(diǎn)擊啟動(dòng)LoginActivity兵琳,但是由于 Robolectric 是一個(gè)單元測(cè)試框架,它并不會(huì)真正啟動(dòng)LoginActivity骇径,所以我們可以通過(guò)查看 WelcomeActivity是否發(fā)出了正確的intent即可:

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

    @Test
    public void clickingLogin_shouldStartLoginActivity() {
        WelcomeActivity activity = Robolectric.setupActivity(WelcomeActivity.class);
        activity.findViewById(R.id.login).performClick();

        Intent expectedIntent = new Intent(activity, LoginActivity.class);
        Intent actual = ShadowApplication.getInstance().getNextStartedActivity();
        assertEquals(expectedIntent.getComponent(), actual.getComponent());
    }
}

更多 [Robolertic] Sample躯肌,請(qǐng)查看:robolectric-samples

配置 Robolectric

  • @Config Annotation
    為一個(gè)類(lèi)或者方法進(jìn)行配置,可以使用注解@Config 破衔,該注解可應(yīng)用于類(lèi)和方法上清女;方法上的注解會(huì)覆蓋類(lèi)上注解。
    如果你發(fā)現(xiàn)在多個(gè)測(cè)試用例上注解了相同內(nèi)容晰筛,那么你可以創(chuàng)建一個(gè)基類(lèi)嫡丙,將注解移到基類(lèi)上即可拴袭。
  @Config(sdk=JELLYBEAN_MR1,
      manifest="some/build/path/AndroidManifest.xml",
      shadows={ShadowFoo.class, ShadowBar.class})
  public class SandwichTest {
  }
  • Configurables - 配置屬性
  1. Configure SDK Level - 配置 SDK 版本
    Robolectric 默認(rèn)會(huì)以manifest中的targetSdkVersion運(yùn)行你的測(cè)試代碼。如果你想要代碼運(yùn)行在其他的 SDK 中曙博,可以使使用 sdk拥刻,minSdkmaxSdk屬性。
@Config(sdk = { JELLY_BEAN, JELLY_BEAN_MR1 })
public class SandwichTest {

    public void getSandwich_shouldReturnHamSandwich() {
      // will run on JELLY_BEAN and JELLY_BEAN_MR1
    }

    @Config(sdk = KITKAT)
    public void onKitKat_getSandwich_shouldReturnChocolateWaferSandwich() {
      // will run on KITKAT
    }
    
    @Config(minSdk=LOLLIPOP)
    public void fromLollipopOn_getSandwich_shouldReturnTunaSandwich() {
      // will run on LOLLIPOP, M, etc.
    }
}
  1. Configure Application Class - 配置 Application 類(lèi)
    Robolectric 默認(rèn)會(huì)創(chuàng)建在manifest中指定的Application實(shí)例父泳,如果你想要自定義另一個(gè)Application實(shí)現(xiàn)般哼,可以進(jìn)行如下設(shè)置:
@Config(application = CustomApplication.class)
public class SandwichTest {

    @Config(application = CustomApplicationOverride.class)
    public void getSandwich_shouldReturnHamSandwich() {
    }
}
  1. Configure Resource and Asset Paths - 配置 ResourceAsset路徑
    Robolectric 對(duì)于 Gradle 和 Maven,有默認(rèn)提供的配置惠窄,但是它也允許你自己自定義manifest蒸眠,resourceassets的路徑。這個(gè)特定對(duì)于自定義構(gòu)建系統(tǒng)是十分有用的杆融,你可以自己指定這些屬性:
@Config(resourceDir = "some/build/path/res")
public class SandwichTest {

    @Config(assetDir = "other/build/path/ham-sandwich/res")
    public void getSandwich_shouldReturnHamSandwich() {
    }
}

更多配置詳情楞卡,請(qǐng)查看:Configuring Robolectric

Driving the Activity Lifecycle - 控制 Activity 生命周期

  1. 獲取一個(gè)初始化的Activity
Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class).create().get();

上面的代碼會(huì)創(chuàng)建一個(gè)MyAwesomeActivity實(shí)例,并且經(jīng)歷了生命周期onCreate脾歇。

  1. 測(cè)試事件在onCreate未發(fā)生蒋腮,在onResume發(fā)生
ActivityController controller = Robolectric.buildActivity(MyAwesomeActivity.class).create().start();
Activity activity = controller.get();
// assert that something hasn't happened
activityController.resume();
// assert it happened!
  1. 模擬使用Intent啟動(dòng)Activity
Intent intent = new Intent(Intent.ACTION_VIEW);
Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class).withIntent(intent).create().get();
  1. 模擬Activity異常恢復(fù)啟動(dòng)
Bundle savedInstanceState = new Bundle();
···
Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class)
    .create()
    .restoreInstanceState(savedInstanceState)
    .get();

更多ActivityController方法說(shuō)明介劫,請(qǐng)查看文檔:ActivityController

  1. 控制Activity生命周期徽惋,并執(zhí)行控件操作
Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class).create().start().resume().visible().get();
// now you can interacte with the views inside the Activity

上面代碼的visible()表達(dá)的是Activity的視圖可見(jiàn),visible()調(diào)用后我們就可以對(duì)Activity視圖進(jìn)行操作座韵。因?yàn)樵谡鎸?shí)的 Android app 中险绘,Activity的視圖層級(jí)是在onCreate()調(diào)用后某個(gè)時(shí)間后才附著到Activity上的,在此之前誉碴,Activity上的視圖是不可見(jiàn)的宦棺,這意味著你不能對(duì)其視圖進(jìn)行點(diǎn)擊等操作,Activity視圖層級(jí)在Activity經(jīng)歷onPostResume()后才會(huì)附著到窗口上黔帕。與其臆測(cè)視圖更新為可見(jiàn)代咸,Robolectric 為開(kāi)發(fā)者提供了這種自己控制視圖可見(jiàn)性的功能。

所以成黄,當(dāng)你想操作Activity界面視圖(views)時(shí)呐芥,你應(yīng)當(dāng)在create()后調(diào)用visible()

Using Add-On Modules - 使用附加模塊

為了減小測(cè)試應(yīng)用外部依賴(lài)的數(shù)量奋岁,Robolectric 影子類(lèi)被切分多個(gè)附加模塊思瘟。Robolectric 主模塊只提供了基礎(chǔ) Android SDK 影子類(lèi),其他一些類(lèi)似appcompatsupport library的影子類(lèi)在其他的附加模塊中闻伶。下表列舉了附件模塊影子類(lèi)包名:

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

對(duì)于上面列舉的附加模塊最新版本滨攻,可以在 Maven 中進(jìn)行查詢(xún)。

參考

用Robolectric來(lái)做Android unit testing

Robolectric, Unit testing framework for Android

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市光绕,隨后出現(xiàn)的幾起案子女嘲,更是在濱河造成了極大的恐慌,老刑警劉巖诞帐,帶你破解...
    沈念sama閱讀 210,914評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件欣尼,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡景埃,警方通過(guò)查閱死者的電腦和手機(jī)媒至,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評(píng)論 2 383
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)谷徙,“玉大人拒啰,你說(shuō)我怎么就攤上這事⊥昊郏” “怎么了谋旦?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,531評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)屈尼。 經(jīng)常有香客問(wèn)我册着,道長(zhǎng),這世上最難降的妖魔是什么脾歧? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,309評(píng)論 1 282
  • 正文 為了忘掉前任甲捏,我火速辦了婚禮,結(jié)果婚禮上鞭执,老公的妹妹穿的比我還像新娘司顿。我一直安慰自己,他們只是感情好兄纺,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,381評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布大溜。 她就那樣靜靜地躺著,像睡著了一般估脆。 火紅的嫁衣襯著肌膚如雪钦奋。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,730評(píng)論 1 289
  • 那天疙赠,我揣著相機(jī)與錄音付材,去河邊找鬼。 笑死圃阳,一個(gè)胖子當(dāng)著我的面吹牛伞租,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播限佩,決...
    沈念sama閱讀 38,882評(píng)論 3 404
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了祟同?” 一聲冷哼從身側(cè)響起作喘,我...
    開(kāi)封第一講書(shū)人閱讀 37,643評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎晕城,沒(méi)想到半個(gè)月后泞坦,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,095評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡砖顷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,448評(píng)論 2 325
  • 正文 我和宋清朗相戀三年贰锁,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片滤蝠。...
    茶點(diǎn)故事閱讀 38,566評(píng)論 1 339
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡豌熄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出物咳,到底是詐尸還是另有隱情锣险,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評(píng)論 4 328
  • 正文 年R本政府宣布览闰,位于F島的核電站芯肤,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏压鉴。R本人自食惡果不足惜崖咨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,829評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望油吭。 院中可真熱鬧击蹲,春花似錦、人聲如沸上鞠。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,715評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)芍阎。三九已至世曾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間谴咸,已是汗流浹背轮听。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,945評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留岭佳,地道東北人血巍。 一個(gè)月前我還...
    沈念sama閱讀 46,248評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像珊随,于是被迫代替她去往敵國(guó)和親述寡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子柿隙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評(píng)論 2 348

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

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,729評(píng)論 25 707
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)鲫凶,斷路器禀崖,智...
    卡卡羅2017閱讀 134,626評(píng)論 18 139
  • 標(biāo)簽(空格分隔): Android 單元測(cè)試的好處:Martin Fowler在《重構(gòu)》里面還解釋了為什么單元測(cè)試...
    背影殺手不太冷閱讀 5,812評(píng)論 3 25
  • 參考文檔 Robolectric 簡(jiǎn)介 Instrumentation 與 Roboletric 都是針對(duì) And...
    三季人閱讀 1,415評(píng)論 2 0
  • 昨晚讀高原的那本《把青春唱完》,里面寫(xiě)道:“……那會(huì)兒也不是因?yàn)橛惺裁词伦龅綐O致才開(kāi)心的螟炫,反而是根本沒(méi)有什么目的波附,...
    猜不中的尾聲閱讀 314評(píng)論 5 4