前言
在博客Android單元測試之PowerMockito,主要介紹PowerMockito的使用和對Java測試用例的強(qiáng)大支持。但對于Android app開發(fā)來說,寫起單元測試很痛苦:一方面單元測試需要運(yùn)行在模擬器上或者真機(jī)上,不僅麻煩而且緩慢;另一方面,一些依賴Android SDK的對象(如Activity,Button等)的測試非常頭疼。Robolectric可以解決此類問題,它的設(shè)計(jì)思路便是通過實(shí)現(xiàn)一套JVM能運(yùn)行的Android代碼,從而做到脫離Android環(huán)境進(jìn)行測試。本文將結(jié)合項(xiàng)目對Robolectric做一個(gè)簡單介紹,并列舉在實(shí)踐踩的各種坑。
Robolectric簡介
我們可以使用Android提供的Instrumentation系統(tǒng)如ActivityUnitTestCase、ActivityInstrumentationTestCase2,將單元測試代碼運(yùn)行在模擬器或者是真機(jī)上。雖然這種方式可以work,但是速度非常慢,因?yàn)槊看芜\(yùn)行一次單元測試,都需要將整個(gè)項(xiàng)目打包成apk,上傳到模擬器或真機(jī)上,就跟運(yùn)行了一次app似得,這個(gè)顯然不是單元測試該有的速度。此外,Google開源的測試框架如UIAutomator和Espresso也是基于Instrumentation的,更偏向于UI方面的自測化測試,要是應(yīng)用在單元測試上速度也是不敢恭維的。
對了,說一句題外話,感興趣的同學(xué)可以看一下ActivityUnitTestCase和ActivityInstrumentationTestCase2的源碼,你會(huì)驚奇地發(fā)現(xiàn),它們的實(shí)現(xiàn)方式還是有所區(qū)別,雖然都是依賴Instrumentation把Activity加載起來,運(yùn)行在同一個(gè)進(jìn)程中,但ActivityUnitTestCase是運(yùn)行在UI主線程中的,而ActivityInstrumentationTestCase2是運(yùn)行在子線程中的,所以在實(shí)際的使用中還是有區(qū)別的,ActivityUnitTestCase可以直接操控UI,而ActivityInstrumentationTestCase2則是不行,需要借助于runOnUiThread()方法來更新UI,否則會(huì)拋異常。
言歸正傳吧,我們還是接著說Robolectric。Robolectric通過實(shí)現(xiàn)一套JVM能運(yùn)行的Android代碼,然后在unit test運(yùn)行的時(shí)候去截取android相關(guān)的代碼調(diào)用,然后轉(zhuǎn)到自己實(shí)現(xiàn)的代碼去執(zhí)行這個(gè)調(diào)用的過程。舉個(gè)例子說明一下,比如Android里面有個(gè)類叫Button,Robolectric則實(shí)現(xiàn)了一個(gè)叫ShadowButton類。這個(gè)類基本上實(shí)現(xiàn)了Button的所有公共接口。假設(shè)你在unit test里面寫到String text = button.getText().toString();,在這個(gè)unit test運(yùn)行時(shí),Robolectric會(huì)自動(dòng)判斷你調(diào)用了Android相關(guān)的代碼button.getText(),在底層截取這個(gè)調(diào)用過程,轉(zhuǎn)到ShadowButton的getText方法來執(zhí)行。而ShadowButton是真正實(shí)現(xiàn)了getText這個(gè)方法的,所以這個(gè)過程便可以正常執(zhí)行。
除了實(shí)現(xiàn)Android里面的類的現(xiàn)有接口,Robolectric還做了另外一件事情,極大地方便了unit testing的工作。那就是他們給每個(gè)Shadow類額外增加了很多接口,方便我們讀取對應(yīng)Android類的一些狀態(tài)。比如ImageView有一個(gè)方法叫setImageResource(resourceId),然而并沒有一個(gè)對應(yīng)的getter方法叫g(shù)etImageResourceId(),這樣你是沒有辦法測試這個(gè)ImageView是不是顯示了你想要的image。而在Robolectric實(shí)現(xiàn)的ShadowImageView里面,則提供了getImageResourceId()這個(gè)接口,你可以用來測試它是否正確的顯示了你想要的image。
Robolectric入門
build.gradle配置:
dependencies {
testCompile "org.robolectric:robolectric:3.3.2"
}
注解配置:
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 23)
public class ExampleRobolectricTestCase {
......
}
說明:上面配置的是RobolectricTestRunner,而不是RobolectricGradleTestRunner,在Robolectric之前的版本是有這個(gè)RobolectricGradleTestRunner,但在最新的版本上卻沒有了,也不知道是為什么。但是有一點(diǎn),使用最新版本后,倒是沒有出現(xiàn)找不到資源文件res的警告。最新的Robolectric最高可支持Android API 23。
Android Studio環(huán)境配置:
1.在Build Variants面板中,將Test Artifact切換成Unit Tests模式,不過在新版本的Android Studio已經(jīng)不需要做這項(xiàng)配置,如下圖:

2.Working directory設(shè)置
如果在運(yùn)行測試方法過程中遇見如下異常:
java.io.FileNotFoundException: build\intermediates\bundles\debug\AndroidManifest.xml
......
或者如下警告:
No such manifest file: build/intermediates/bundles/debug/AndroidManifest.xml
......
解決的方式就是將Working directory的值設(shè)置為$MODULE_DIR$。
第一步設(shè)置如下:

第二步設(shè)置如下:

設(shè)置完畢后,再次run就可以了。
Robolectric實(shí)戰(zhàn)
首先在build.gradle中的完整配置如下:
testCompile "junit:junit:4.12"
testCompile "org.assertj:assertj-core:1.7.0"
testCompile "org.robolectric:robolectric:3.3.2"
// PowerMock brings in the mockito dependency
testCompile 'org.powermock:powermock-module-junit4:1.6.5'
testCompile 'org.powermock:powermock-module-junit4-rule:1.6.5'
testCompile 'org.powermock:powermock-api-mockito:1.6.5'
testCompile 'org.powermock:powermock-classloading-xstream:1.6.5'
從配置中,可以看出在實(shí)際運(yùn)用中,我們是使用JUnit4+Mockito+PowerMockito+Robolectric,這是一個(gè)牛逼的組合,在寫單元測試用例時(shí)簡直溜得飛起,通過PowerMockito彌補(bǔ)Mockito測試框架不能mock靜態(tài)方法、final方法和private方法的不足,還可以在JVM中就可以很方便的調(diào)用Android相關(guān)的類和方法,速度也比較快。
然后定義抽象類BaseRobolectricTestCase:
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowLog.class}, constants = BuildConfig.class, sdk = 23)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*", "org.json.*", "sun.security.*", "javax.net.*"})
public abstract class BaseRobolectricTestCase {
@Rule
public PowerMockRule rule = new PowerMockRule();
private static boolean hasInited = false;
@Before
public void setUp() {
ShadowLog.stream = System.out;
if (!hasInited) {
initRxJava();
hasInited = true;
}
MockitoAnnotations.initMocks(this);
}
public Application getApplication() {
return RuntimeEnvironment.application;
}
public Context getContext() {
return RuntimeEnvironment.application;
}
private void initRxJava() {
RxJavaPlugins.getInstance().registerSchedulersHook(new RxJavaSchedulersHook() {
@Override
public Scheduler getIOScheduler() {
return Schedulers.immediate();
}
});
RxAndroidPlugins.getInstance().registerSchedulersHook(new RxAndroidSchedulersHook() {
@Override
public Scheduler getMainThreadScheduler() {
return Schedulers.immediate();
}
});
}
}
這個(gè)抽象類代碼比較多,主要是設(shè)置Robolectric單元測試的運(yùn)行環(huán)境,方便在單元測試用例代碼中進(jìn)行復(fù)用。具體分下一下:
-
@RunWith(RobolectricTestRunner.class)通過注解定義Robolectric運(yùn)行的TestRunner; -
@Config(shadows = {ShadowLog.class}, constants = BuildConfig.class, sdk = 23)通過配置shadows = {ShadowLog.class}和ShadowLog.stream = System.out;來設(shè)置Android log輸出方式,使得單元測試運(yùn)行時(shí)在控制臺(tái)中可以看到Android代碼中打印出的log日志; -
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*", "org.json.*", "sun.security.*", "javax.net.*"})通過PowerMockIgnore注解定義所忽略的package路勁,防止所定義的package路徑下的class類被PowerMockito測試框架mock; - 在setUp()方法中調(diào)用
MockitoAnnotations.initMocks(this);初始化PowerMockito注解,為@PrepareForTest(YourStaticClass.class)注解提供支持; - 在代碼中,我們可以看到定義了兩個(gè)基本方法getApplication()和getContext(),在寫測試代碼中使用起來很方便,就像在Activity一樣,增加測試的可讀性;
- 如果項(xiàng)目中使用了rxjava框架,在對rxjava相關(guān)的代碼進(jìn)行單元測試時(shí),通過initRxJava()方法將異步處理轉(zhuǎn)化為同步處理,如此一來方便單元測試驗(yàn)證;
最后編寫Activity測試用例代碼:
public class ComplaintActivityTest extends BaseRobolectricTestCase {
@Test
@PrepareForTest({AppUtil.class, OAuthManager.class, NetUtil.class})
public void jumpCompensate() throws Exception {
PowerMockito.mockStatic(AppUtil.class);
PowerMockito.when(AppUtil.getVersionName()).thenReturn("1.4.0");
PowerMockito.mockStatic(OAuthManager.class);
OAuthManager mockOAuth = PowerMockito.mock(OAuthManager.class);
PowerMockito.when(OAuthManager.getInstance()).thenReturn(mockOAuth);
PowerMockito.when(mockOAuth.getSargerasToken()).thenReturn("c97faa92-34ea-4248-a19e-9a9fb848b29b");
AppApplication.mInstance = getApplication();
PowerMockito.mockStatic(NetUtil.class);
PowerMockito.when(NetUtil.isNetworkConnected(AppApplication.getInstance())).thenReturn(true);
PreferenceUtil.init();
PersistentPreferenceUtil.init();
ComplaintActivity complaintActivity = Robolectric.buildActivity(ComplaintActivity.class).create().get();
assertNotNull(complaintActivity);
complaintActivity.jumpCompensate();
Intent expectedIntent = new Intent(complaintActivity, HelpActivity.class);
ShadowActivity shadowActivity = Shadows.shadowOf(complaintActivity);
Intent actualIntent = shadowActivity.getNextStartedActivity();
Assert.assertEquals(expectedIntent.getComponent().getClassName(), actualIntent.getComponent().getClassName());
}
}
上面前一部分代碼主要設(shè)置ComplaintActivity運(yùn)行所依賴的屬性,這也是在單元測試最為繁瑣的地方,因?yàn)椴皇沁\(yùn)行在真實(shí)的Android環(huán)境中。具體分析如下:
- 通過注解
@PrepareForTest({AppUtil.class, OAuthManager.class, NetUtil.class})定義PowerMockito要mock的類; - 在Robolectric中讀取不到apk的版本號,通過
PowerMockito.when(AppUtil.getVersionName()).thenReturn("1.4.0");mock指定AppUtil.getVersionName()的返回值"1.4.0",即版本號; - 通過
AppApplication.mInstance = getApplication();使用Robolectric運(yùn)行環(huán)境中的application對AppApplication.mInstance進(jìn)行依賴注入,因?yàn)樵诤芏囝愔卸紩?huì)用到AppApplication.mInstance進(jìn)行初始化,例如SharedPreference、SQlite、單例類等,
PreferenceUtil.init();
PersistentPreferenceUtil.init();
上面代碼就需要依賴AppApplication.mInstance進(jìn)行初始化;
-
ComplaintActivity complaintActivity = Robolectric.buildActivity(ComplaintActivity.class).create().get();使用Robolectric創(chuàng)建ComplaintActivity對象,其中create()方法就是對應(yīng)于調(diào)用Activity生命周期的onCreate()方法,此外Robolectric支持鏈?zhǔn)秸{(diào)用如:Robolectric.buildActivity(ComplaintActivity.class).create().resume().get();; -
assertNotNull(complaintActivity);驗(yàn)證complaintActivity是否跑起來; - 最后一部分代碼就是調(diào)用jumpCompensate方法進(jìn)行跳轉(zhuǎn),驗(yàn)證跳轉(zhuǎn)的Intent是否符合預(yù)期;
至于其他的一些如Fragment、Dialog、Toast等驗(yàn)證,可以參考這篇博客,這里就不展開。
Robolectric常見的坑
1.Application空指針問題
這是因?yàn)镾haredPreferences和單例等類初始化時(shí)需要依賴Application對象,我們常見的用法是使用Application.getApplication()方法來獲取,在Robolectric中則是需要使用RuntimeEnvironment.application來進(jìn)行替換,上面就是通過依賴的方式進(jìn)行替換。
2. AppCompatActivity錯(cuò)誤
假如你在Robolectric的@Config注解中配置了manifest = Config.NONE,那就完蛋了,因?yàn)樵诰W(wǎng)上根本找不解決的方法,你遇到如下異常不能使用support V7包的類:
java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.
at android.support.v7.app.AppCompatDelegateImplV7.createSubDecor(AppCompatDelegateImplV7.java:343)
at android.support.v7.app.AppCompatDelegateImplV7.ensureSubDecor(AppCompatDelegateImplV7.java:312)
at android.support.v7.app.AppCompatDelegateImplV7.initWindowDecorActionBar(AppCompatDelegateImplV7.java:172)
at android.support.v7.app.AppCompatDelegateImplBase.getSupportActionBar(AppCompatDelegateImplBase.java:88)
at android.support.v7.app.AppCompatActivity.getSupportActionBar(AppCompatActivity.java:110)
at me.ele.shopcenter.components.BaseActivity.initActionBar(BaseActivity.java:104)
at me.ele.shopcenter.components.BaseActivity.onCreate(BaseActivity.java:52)
at me.ele.shopcenter.ui.order.ComplaintActivity.onCreate(ComplaintActivity.java:93)
at android.app.Activity.performCreate(Activity.java:6251)
at org.robolectric.util.ReflectionHelpers.callInstanceMethod(ReflectionHelpers.java:231)
解決的方式就是去掉manifest = Config.NONE配置,這是坑爹的,我就遇到這個(gè)錯(cuò)誤,花了好長一段時(shí)間才發(fā)現(xiàn)是這個(gè)配置導(dǎo)致的。
3.Asset文件路徑錯(cuò)誤
需要用到context.getAssets().open("XXX")加載asset目錄下的文件時(shí),要是遇到以下錯(cuò)誤:
java.io.FileNotFoundException: build/intermediates/bundles/debug/assets/https.cer (No such file or directory)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.<init>(FileInputStream.java:138)
at org.robolectric.res.FileFsFile.getInputStream(FileFsFile.java:84)
at org.robolectric.shadows.ShadowAssetManager.open(ShadowAssetManager.java:319)
at android.content.res.AssetManager.open(AssetManager.java)
解決方式是,不要用AssetManager來加載文件,而是自己使用Java API來加載文件,如:
new FileInputStream(new File("/Users/michaelzhong/Desktop/shop/talaris_shop_center/app/src/main/assets/https.cer"));
這個(gè)方式有點(diǎn)丑,需要用到你要加載的文件的絕對路徑,靈活性低,不方便移植,不過這是我目前想到的解決方式。
4.找不到android.net.http.AndroidHttpClient的類文件
在Android API23開始,google就移除了HttpClient相關(guān)的類,有兩種方法解決上述問題。
方法一:在build.gradle添加應(yīng)用useLibrary ‘org.apache.http.legacy’
方法二:在test目錄下添加HttpClient類(記得包名為android.net.http),如下:

說明:推薦使用第二種方式,第二種方法正式打包并不會(huì)把HttpClient的類加入,減少了包中無用的資源。
小結(jié)
在實(shí)際的使用中,Robolectric需要踩很多坑的,不過貴在嘗試。至此,單元測試系列博客已經(jīng)完結(jié),主要分了四篇博客來講述。非常感謝您對本篇博客的支持,要是有什么不足歡迎指正!