前言
前面我們介紹了單元測(cè)試框架 JUnit 和 Mockito 的使用(詳情查看:單元測(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)行如下配置:
Sample
Activity
-
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 - 配置屬性
- Configure SDK Level - 配置 SDK 版本
Robolectric 默認(rèn)會(huì)以manifest
中的targetSdkVersion
運(yùn)行你的測(cè)試代碼。如果你想要代碼運(yùn)行在其他的 SDK 中曙博,可以使使用sdk
拥刻,minSdk
和maxSdk
屬性。
@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.
}
}
-
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() {
}
}
-
Configure Resource and Asset Paths - 配置
Resource
和Asset
路徑
Robolectric 對(duì)于 Gradle 和 Maven,有默認(rèn)提供的配置惠窄,但是它也允許你自己自定義manifest
蒸眠,resource
和assets
的路徑。這個(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 生命周期
- 獲取一個(gè)初始化的
Activity
Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class).create().get();
上面的代碼會(huì)創(chuàng)建一個(gè)MyAwesomeActivity
實(shí)例,并且經(jīng)歷了生命周期onCreate
脾歇。
- 測(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!
- 模擬使用
Intent
啟動(dòng)Activity
Intent intent = new Intent(Intent.ACTION_VIEW);
Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class).withIntent(intent).create().get();
- 模擬
Activity
異常恢復(fù)啟動(dòng)
Bundle savedInstanceState = new Bundle();
···
Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class)
.create()
.restoreInstanceState(savedInstanceState)
.get();
更多ActivityController
方法說(shuō)明介劫,請(qǐng)查看文檔:ActivityController
- 控制
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)似appcompat
和support 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)。