簡介
Robolectric
通過實現(xiàn)一套JVM
能運行的Android代碼,從而做到脫離Android環(huán)境進(jìn)行測試。在unit test
運行的時候去截取android相關(guān)的代碼調(diào)用鸿捧,然后轉(zhuǎn)到他們的他們實現(xiàn)的代碼去執(zhí)行這個調(diào)用的過程屹篓。因為不需要再真機(jī)上運行,所以測試速度相較于Instrumentation Test要快很多匙奴。
Robolectric是個非常強大好用的單元測試框架堆巧。雖然使用的過程中肯定也會遇到問題,我個人就遇到不少問題泼菌,尤其是跟第三方的library比如Retrofit谍肤、ActiveAndroid結(jié)合使用的時候,會有不少問題哗伯,但瑕不掩瑜荒揣,我們依然可以用它完成很大部分的unit testing工作。
Robolectric 3.1(目前最先版本為3.2.2)已支持針對非AndroidSdk的類做Shadow笋颤,但是不支持Powermock乳附。如果使用3.0的robolectric 就可以支持Powermock, 如果選擇3.1以上的版本的robolectric 就可以支持非AndroidSdk的類的Shadow。本方案中采用Robolectric3.2.2進(jìn)行Android相關(guān)的測試伴澄。
具體實踐
添加依賴
dependencies {
testCompile "org.robolectric:robolectric:3.2.2"
//robolectric針對support-v4的shadows
testCompile "org.robolectric:shadows-support-v4:3.0"
}
Robolectric在第一次運行時赋除,會下載一些sdk依賴包,每個sdk依賴包至少50M非凌,而 https://oss.sonatype.org 服務(wù)器比較慢举农,導(dǎo)致下載速度非常慢。所以最好手動下載所需要的文件敞嗡。例如,需要下載以下文件:
Downloading: org/robolectric/android-all/6.0.1_r3-robolectric-0/android-all-6.0.1_r3-robolectric-0.jar from repository sonatype at https://oss.sonatype.org/content/groups/public/
Transferring 56874K from sonatype
解決方案:
- 從http://repo1.maven.org/maven2/org/robolectric/android-all/6.0.1_r3-robolectric-0/android-all-6.0.1_r3-robolectric-0.jar中下載
android-all-6.0.1_r3-robolectric-0.jar
- 將jar文件放置在本地maven倉庫地址中颁糟,例如:
C:\Users\Administrator\.m2\repository\org\robolectric\android-all\6.0.1_r3-robolectric-0
重寫TestRunner
在非app的module中添加Robolectric測試框架時,很可能會出現(xiàn)以下異常:
java.lang.RuntimeException: build\intermediates\bundles\debug\AndroidManifest.xml not found or not a file; it should point to your project's AndroidManifest.
發(fā)生此異常喉悴,是因為無法正常獲取AndroidManifest文件棱貌。此時就需要重寫TestRunner,可以從RobolectricGradleTestRunner
繼承,然后覆蓋getAppManifest
方法箕肃,構(gòu)建自己想要的特殊的AndroidManifest
對象來實現(xiàn)對AndroidManifest,assets,res資源的控制婚脱。
public class MyRobolectricTestRunner extends RobolectricTestRunner {
... ...
@Override
protected AndroidManifest getAppManifest(Config config) {
int nameLength = projectName.length();
String rootPath = System.getProperty("user.dir", "./");
int index = rootPath.indexOf(projectName);
if (index == -1) {
throw new RuntimeException("project name not found in user.dir");
}
//獲取項目的根目錄
rootPath = rootPath.substring(0, index + nameLength);
String manifestProperty = rootPath + "/module/src/main/AndroidManifest.xml";
String resProperty = rootPath + "/module/src/main/res";
String assetsProperty = rootPath + "/module/src/main/assets";
return new AndroidManifest(
Fs.fileFromPath(manifestProperty),
Fs.fileFromPath(resProperty),
Fs.fileFromPath(assetsProperty)) {
@Override
public int getTargetSdkVersion() {
return MAX_SDK_SUPPORTED_BY_ROBOLECTRIC;
}
};
}
}
隔離原Application依賴
如果用 Robolectric 單元測試,不配置Application 勺像,就會調(diào)用原來的項目的Application障贸,而 App 有很多第三方庫依賴。于是吟宦,執(zhí)行 App 生命周期時篮洁, robolectric 就容易報錯。
正確配置 Application 方式:
- 自定義一個用于單元測試的 RoboApplication.class
- 配置Application殃姓。有以下兩種方式:
方式一:在單元測試 XXTest 加上 @Config(application = RoboApplication.class) 袁波。
@Config(application = RoboApplication.class)
public class XXXTest { }
方式二:在TestRunner中設(shè)置Application
@Override
protected Config buildGlobalConfig() {
return new Config.Builder()
.setApplication(RoboApplication.class)
.build();
}
日志輸出
我們在寫UT的過程瓦阐,其實也是在調(diào)試代碼,而日志輸出對于代碼調(diào)試起到極大的作用锋叨。而Robolectric
對日志輸出的支持其實非常簡單垄分。只需要在每個TestCase的setUp( )方法或者Application的onCreate()方法中中添加一句命令:
@Before
public void setUp() throws URISyntaxException {
//輸出日志
ShadowLog.stream = System.out;
}
此時,無論是功能代碼還是測試代碼中的 Log.i()之類的相關(guān)日志都將輸出在控制面板中娃磺。
單元測試示例
測試代碼是放在app/src/test
下面的薄湿,測試類的位置最好跟被測試類的位置對應(yīng),比如MainActivity放在 app/src/main/java/com/robo/test/MainActivity.java
那么對應(yīng)的測試類MainActivityTest最好放在app/src/test/java/com//robo/test/MainActivityTest.java
下面以XfConsultantProfileActivity類的測試為例偷卧,進(jìn)行簡單的介紹:
新建Unit Test
通過注解配置TestRunner等基本信息
@RunWith(MyRobolectricTestRunner.class)
@Config(constants = BuildConfig.class)
public class ProfileActivityUnitTest {
private ProfileActivity mActivity;
@Before //在運行test之前豺瘤,進(jìn)行一些初始化操作
public void setUp() throws Exception {
}
@Test //測試方法
public void testFragment() throws InterruptedException {
}
}
啟動Activity
可以通過setupActivity()
方法來直接啟動Activity
mActivity = Robolectric.setupActivity(ProfileActivity.class);
如果要啟動的Activity需要從Intent中獲取額外的數(shù)據(jù),那么就需要使用buildActivity()
方法來獲取ActivityController
听诸。通過ActivityController
坐求,不僅可以設(shè)置Activity的Intent,還可以創(chuàng)建Activity和控制Activity的生命周期晌梨。
Intent intent = new Intent();
intent.putExtra("id", 76270);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setClass(RuntimeEnvironment.application, DetailActivity.class);
mActivity = Robolectric.buildActivity(ProfileActivity.class)
.withIntent(intent).setup().get();
驗證Fragment
通過Activity對象獲取Fragment:
//獲取Fragment
List<Fragment> fragmentList = mActivity.getSupportFragmentManager().getFragments();
ProfileFragment mFragment = null;
if (fragmentList.get(0) instanceof ProfileFragment) {
mFragment = (ProfileFragment) fragmentList.get(0);
}
assertNotNull(mFragment);
也可以脫離Activity桥嗤,直接啟動Fragment實體。對于android.app.Fragment
直接使用SupportFragmentTestUtil
的startFragment
方法啟動Fragment仔蝌。
ProfileFragment mFragment = ProfileFragment
.getInstance(76270);
FragmentTestUtil.startFragment(mFragment);
assertNotNull(mFragment.getView());
而對于support-v4
的Fragment需要使用
SupportFragmentTestUtil
的startFragment
方法啟動Fragment泛领,且需要添加相應(yīng)的依賴:
dependencies {
//robolectric針對support-v4的shadows
testCompile "org.robolectric:shadows-support-v4:3.2.2"
}
驗證ListView
//執(zhí)行ListView的Item點擊事件
ListView listView = (ListView) mFragment.findViewById(R.id.listview);
View view = listView.getAdapter().getView(1,null,null);
listView.performItemClick(view,0,0);
驗證RecyclerView
RecyclerView recyclerView = (RecyclerView) housesLayout.findViewById(R.id.housesView);
//需要添加下面這兩句代碼,RecyclerView才會加載布局
recyclerView.measure(0, 0);
recyclerView.layout(0, 0, 100, 10000);
//指向Item點擊事件
recyclerView.findViewHolderForAdapterPosition(0).itemView.performClick();
處理網(wǎng)絡(luò)回調(diào)
在后臺線程中請求網(wǎng)絡(luò)敛惊,請求完成后在UI線程里通過Listener接口通知請求完成渊鞋,并傳遞請求回來的數(shù)據(jù)。這時需要如何處理呢瞧挤?
在使用Robolectric
框架測試需要在UI線程執(zhí)行的邏輯時锡宋,在Android平臺UI線程會輪詢消息隊列,然后從消息隊列里取出消息特恬,并將消息分發(fā)給Handler處理执俩,UI線程執(zhí)行的是輪詢消息隊列的死循環(huán)。但是在Robolectric
框架中運行時癌刽,UI線程默認(rèn)情況下并不會輪詢消息隊列役首,而需要在測試用例代碼里主動驅(qū)動UI線程
從消息隊列里取出消息進(jìn)行分發(fā)。測試用例執(zhí)行時并不在UI線程妒穴,而是在單獨的線程中,所以它可以主動驅(qū)動UI線程分發(fā)消息摊崭。
所以在執(zhí)行網(wǎng)絡(luò)請求后讼油,需要主要驅(qū)動UI線程輪詢消息隊列,獲取返回的數(shù)據(jù)呢簸。從下面的代碼可以看到我們可以通過獲取Scheduler對象來判斷消息隊列中是否有消息矮台,并調(diào)用Scheduler的runOneTask方法進(jìn)行消息分發(fā)乏屯,這樣就驅(qū)動了主線程進(jìn)行消息輪詢,
//獲取主線程的消息隊列的調(diào)度者瘦赫,通過它可以知道消息隊列的情況
//并驅(qū)動主線程主動輪詢消息隊列
Scheduler scheduler = Robolectric.getForegroundThreadScheduler();
//因為調(diào)用請求方法后 后臺線程請求需要一段時間才能請求完畢辰晕,然后才會通知主線程
// 所以在這里進(jìn)行等待,直到消息隊列里存在消息
while (scheduler.size() == 0) {
Thread.sleep(500);
}
//輪詢消息隊列确虱,這樣就會在主線程進(jìn)行通知
scheduler.runOneTask();
Shadow
Shadow
是Robolectric的立足之本含友。因此,框架針對Android SDK中的對象校辩,提供了很多Shadow
對象(如Activity和ShadowActivity窘问、TextView和ShadowTextView等),這些Shadow
對象宜咒,豐富了本尊的行為惠赫,能更方便的對Android相關(guān)的對象進(jìn)行測試。從Robolectric 3.1開始已支持針對非Android SDK的類構(gòu)建Shadow了故黑。
- 使用框架提供的Shadow對象
//通過Shadows.shadowOf()可以獲取很多Android對象的Shadow對象
ShadowListView shadowListView = Shadows.shadowOf(listView);
shadowListView.performItemClick(0);
- 如何自定義Shadow對象
以User類為例儿咱,創(chuàng)建自定義 Shadow對象。
@Implements(User.class) //原始對象
public class ShadowUser {
@Implementation //重新實現(xiàn)原始對象中的方法
public long getUserId() {
return (long)70652;
}
}
接下來场晶,需將定義好的Shadow對象混埠,在TestRunner進(jìn)行設(shè)置
private static final Class<?> mShadowClass[] = {ShadowUser.class};
@Override
protected Config buildGlobalConfig() {
return new Config.Builder()
.setShadows(mShadowClass) //設(shè)置shadows
.build();
}