Android單元測試--Robolectric 3.2.2

簡介


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

解決方案:

  1. 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
  2. 將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 方式:

  1. 自定義一個用于單元測試的 RoboApplication.class
  2. 配置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直接使用SupportFragmentTestUtilstartFragment方法啟動Fragment仔蝌。

ProfileFragment mFragment = ProfileFragment
            .getInstance(76270);
FragmentTestUtil.startFragment(mFragment);
assertNotNull(mFragment.getView());

而對于support-v4的Fragment需要使用
SupportFragmentTestUtilstartFragment方法啟動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了故黑。

  1. 使用框架提供的Shadow對象
//通過Shadows.shadowOf()可以獲取很多Android對象的Shadow對象
ShadowListView shadowListView = Shadows.shadowOf(listView);
shadowListView.performItemClick(0);
  1. 如何自定義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();
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市峰搪,隨后出現(xiàn)的幾起案子岔冀,更是在濱河造成了極大的恐慌,老刑警劉巖概耻,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件使套,死亡現(xiàn)場離奇詭異,居然都是意外死亡鞠柄,警方通過查閱死者的電腦和手機(jī)侦高,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來厌杜,“玉大人奉呛,你說我怎么就攤上這事『痪。” “怎么了瞧壮?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長匙握。 經(jīng)常有香客問我咆槽,道長,這世上最難降的妖魔是什么圈纺? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任秦忿,我火速辦了婚禮麦射,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘灯谣。我一直安慰自己潜秋,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布胎许。 她就那樣靜靜地躺著峻呛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪呐萨。 梳的紋絲不亂的頭發(fā)上杀饵,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機(jī)與錄音谬擦,去河邊找鬼切距。 笑死,一個胖子當(dāng)著我的面吹牛惨远,可吹牛的內(nèi)容都是我干的谜悟。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼北秽,長吁一口氣:“原來是場噩夢啊……” “哼葡幸!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起贺氓,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤蔚叨,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后辙培,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蔑水,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年扬蕊,在試婚紗的時候發(fā)現(xiàn)自己被綠了搀别。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡尾抑,死狀恐怖歇父,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情再愈,我是刑警寧澤榜苫,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站翎冲,受9級特大地震影響垂睬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一羔飞、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧檐春,春花似錦逻淌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至俐巴,卻和暖如春骨望,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背欣舵。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工擎鸠, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人缘圈。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓劣光,卻偏偏與公主長得像,于是被迫代替她去往敵國和親糟把。 傳聞我的和親對象是個殘疾皇子绢涡,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,071評論 25 707
  • 標(biāo)簽(空格分隔): Android 單元測試的好處:Martin Fowler在《重構(gòu)》里面還解釋了為什么單元測試...
    背影殺手不太冷閱讀 5,821評論 3 25
  • 我舒開長長的衣袖 舞一曲 水上芭蕾 那柔柔軟軟的展翼 疊出天使之般 嬌艷 如果,可以 在深深邃邃的海底 在無人觸及...
    六月孺子牛閱讀 636評論 7 10
  • 在這里遣疯,我將在Storyboard里使用Autolayout對scrollView實現(xiàn)自動布局雄可!我們要實現(xiàn)如圖界面...
    kysonyangs閱讀 2,803評論 7 10