Android單元測(cè)試-Robolectric 淺析

介紹

Robolectric 測(cè)試框架針對(duì) Android 的組件(包含各種View)進(jìn)行了統(tǒng)一的 Shadow敛苇,使得我們不再依賴模擬器或真機(jī)乡小,直接就單元測(cè)試就可方便地測(cè)試我們的 UI殊鞭。

引入

testCompile "org.robolectric:robolectric:3.1.1"

使用

1.通用 Demo 示例

這里先來一個(gè)簡(jiǎn)單的 Demo, 也是我們經(jīng)常使用的形式:

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class RobolectricTestMainActivity {

    @Test
    public void test() {
        Activity activity = Robolectric.setupActivity(TestMainActivity.class);

        ShadowActivity shadowActivity = Shadows.shadowOf(activity);

        Button button = (Button) activity.findViewById(R.id.btn_test_main);
        TextView textView = (TextView) activity.findViewById(R.id.tv_test_main);

        button.performClick();
        assertThat(textView.getText().toString(), equalTo("Hello"));

        Intent intent = new Intent(activity, TestToastActivity.class);
        activity.startActivity(intent);
        assertThat(shadowActivity.getNextStartedActivity(), equalTo(intent));
    }
}

在真實(shí)的 TestMainActivity 中垦江,存在一個(gè)按鈕和一個(gè)文本框姑躲,當(dāng)點(diǎn)擊按鈕之后撩鹿,將文本框的內(nèi)容修改為 “hello”谦炬。當(dāng)我們通過 RobolectricsetupActivity 構(gòu)造出來一個(gè) Activity 之后,對(duì)其進(jìn)行操作并驗(yàn)證,完全符合我們的預(yù)期結(jié)果键思。

另外础爬,在上面的示例中,針對(duì) Shadow 的使用吼鳞,我們通過真實(shí)的 startActivity 方法啟動(dòng)下一個(gè) Activity看蚜。若此時(shí),我們需要驗(yàn)證其是否啟動(dòng)成功赔桌,就可以使用其對(duì)應(yīng)的 ShadowActivity供炎。在拿到 ShadowActivity 之后,通過獲取其 getNextStartedActivity疾党,就可驗(yàn)證其是否啟動(dòng)成功音诫。

2.Custom Shadow 的使用

初次接觸這個(gè) Shadow 可能有些困惑,我們?cè)?Robolectric 給我們提供的 Shadows 類中雪位,可以發(fā)現(xiàn)其已經(jīng)有很多的 Shadow 實(shí)現(xiàn)竭钝,其以一個(gè) map 的格式存儲(chǔ)真實(shí)類跟 shadow 類對(duì)應(yīng)的關(guān)系:

private static final Map<String, String> SHADOW_MAP = new HashMap<>(250);

static {
 SHADOW_MAP.put("android.widget.AbsListView", "org.robolectric.shadows.ShadowAbsListView");
 SHADOW_MAP.put("android.widget.AbsSeekBar", "org.robolectric.shadows.ShadowAbsSeekBar");
 SHADOW_MAP.put("android.widget.AbsSpinner", "org.robolectric.shadows.ShadowAbsSpinner");
 SHADOW_MAP.put("android.widget.AbsoluteLayout", "org.robolectric.shadows.ShadowAbsoluteLayout");
 SHADOW_MAP.put("android.widget.AbsoluteLayout.LayoutParams", "org.robolectric.shadows.ShadowAbsoluteLayout$ShadowLayoutParams");
 SHADOW_MAP.put("android.database.AbstractCursor", "org.robolectric.shadows.ShadowAbstractCursor");
   **** 省略
}

這里,大概就可以獲悉其的實(shí)現(xiàn)方法雹洗,通過 Shadow 類來替換其對(duì)應(yīng)的真實(shí)方法的實(shí)現(xiàn)香罐,最終達(dá)到的目的就會(huì)使我們的測(cè)試脫離一些底層的具體實(shí)現(xiàn),來達(dá)到我們最快測(cè)試的目的时肿。

若是大家感興趣的話庇茫,可以具體查看相應(yīng)組件類的 Shadow 實(shí)現(xiàn)。當(dāng)然嗜侮,這里我們也可以自定義 Shadow港令,來滿足定制化的需求啥容,這里來個(gè)很簡(jiǎn)單的實(shí)現(xiàn):

  • 定義 Shadow 類
@Implements(Toast.class)
public class CustomShadowToast {

    private static boolean mIsShown;

    public void __constructor__(Context context) {
    }

    @Implementation
    public void show() {
        mIsShown = true;
    }

    public static boolean isToastShowInvoked() {
        return mIsShown;
    }
}

這里以 Toast 為例锈颗,只對(duì)其 show 方法做以實(shí)現(xiàn),當(dāng)調(diào)用了 show 方法之后咪惠,我們將一靜態(tài)變量 mIsShown 標(biāo)記為 true击吱,通過 isToastShowInvoked 方法來進(jìn)行判斷其是否調(diào)用。

需要注意的三點(diǎn):@Implements 注解指定需要對(duì)哪個(gè)類進(jìn)行 shadow遥昧;@Implementation 指定需要對(duì)哪個(gè)方法進(jìn)行替換覆醇;構(gòu)造器需要通過 _constructor_ 來編寫。

  • 測(cè)試調(diào)用
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21, shadows = { CustomShadowToast.class })
public class CustomShadowTest {

    @Test
    public void testToast() {
        Activity activity = Robolectric.setupActivity(TestToastActivity.class);

        Button button = (Button) activity.findViewById(R.id.btn_test_main);
        button.performClick();

        assertThat(CustomShadowToast.isToastShowInvoked(), is(true));
        assertThat(shadowOf(RuntimeEnvironment.application).getShownToasts().size() == 0, is(true));
    }
}

這里要注意的是在 Config 注解中添加我們的 Shadow 類炭臭。在 TestToastActivity 類中永脓,通過 button 的點(diǎn)擊,來隨意顯示一個(gè) Toast 鞋仍,我們是可以發(fā)現(xiàn)自定義 CustomShadowToast 的靜態(tài)變量確實(shí)是調(diào)用了常摧。

不過第二個(gè) assertThat 方法對(duì)顯示的 toast 數(shù)目做判斷,卻發(fā)現(xiàn)個(gè)數(shù)為零。這 shownToasts 數(shù)目的改變落午,是在 ShadowToast 類中谎懦,進(jìn)行添加的,可看代碼:

@Implementation
public void show() {
 shadowOf(RuntimeEnvironment.application).getShownToasts().add(toast);
}

因?yàn)?ShadowToast 類中也對(duì) show 方法做了實(shí)現(xiàn)溃斋,但是其卻被我們自定義實(shí)現(xiàn)給替換掉了界拦。所以我們?cè)谧远x Shadow 實(shí)現(xiàn)的時(shí)候,需要對(duì)這一點(diǎn)謹(jǐn)慎一二梗劫。

另外享甸,我們也有在自定義 Shadow 的時(shí)候,需要持有真實(shí)類的引用在跳,可以直接使用 RealObject 注解枪萄,就像 ShadowToast 一樣:

@Implements(Toast.class)
public class ShadowToast {
   
   // 省略

  @RealObject Toast toast;
}

淺析

相信大家也是同我一樣會(huì)對(duì)這里的 Shadow 實(shí)現(xiàn)頗感興趣的。問題是 Shadow 類是如何跟真實(shí)的類掛上關(guān)系的猫妙?我們?cè)卺槍?duì)真實(shí)類方法的調(diào)用瓷翻,最后卻調(diào)用的是 Shadow 類里面的方法。

以第一個(gè) Demo 中的 ShadowActivity 的獲取為例割坠,查看 shadowOf 方法:

public static ShadowActivity shadowOf(Activity actual) {
 return (ShadowActivity) ShadowExtractor.extract(actual);
}

進(jìn)而再看 ShadowExtractor

public class ShadowExtractor {
  public static Object extract(Object instance) {
    return ((ShadowedObject) instance).$$robo$getData();
  }
}

而其中的 ShadowedObject 就是一個(gè)很簡(jiǎn)單的接口:

public interface ShadowedObject {
  Object $$robo$getData();
}

由此可知齐帚,我們的 Activity 對(duì)象 actual 其實(shí)已經(jīng)實(shí)現(xiàn)了 ShadowedObject 接口。這個(gè)就比較吊了啊彼哼,這里代碼查看到頭对妄,再追溯 Activity 是如何構(gòu)造的,發(fā)現(xiàn)并無什么特別的地方敢朱。那最后只剩 @RunWith 注解的參數(shù) RobolectricTestRunner 類了剪菱,在 runChild 方法中,發(fā)現(xiàn)構(gòu)造 SdkEnvironmentInstrumentingClassLoader 的身影拴签,細(xì)看這個(gè)類孝常,發(fā)現(xiàn)應(yīng)該就是它完成了我們所需要的功能。

首先蚓哩,它繼承了 ClassLoader 构灸,它在 loadClass 中進(jìn)行了重寫,對(duì)由需要由自己進(jìn)行特殊加載的類岸梨,執(zhí)行 findClass 的方法喜颁,否則用父類的 loadClass 方法。

findClass 中曹阔,其使用了 ASM 這個(gè)字節(jié)碼修改庫(kù)半开,來對(duì)我們需要修改的類的字節(jié)碼做修改,使其與我們的 shadow 相綁定赃份。最可證明的就是其中的這段代碼:

classNode.interfaces.add(Type.getInternalName(ShadowedObject.class));

通過 ASM 的 ClassNode 對(duì)象添加了 ShadowedObject 的接口寂拆,與我們之前看到的相吻合。但是類方法是如何替換的,這里的代碼就看的是一頭霧水了漓库。這里先留一個(gè)坑恃慧,以后理解了 Java 的字節(jié)碼,再來填這個(gè)坑渺蒿。若是有小伙伴對(duì)這里也有興趣痢士,可加 QQ 群:289926871 一起交流。

參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末茂装,一起剝皮案震驚了整個(gè)濱河市怠蹂,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌少态,老刑警劉巖城侧,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異彼妻,居然都是意外死亡嫌佑,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門侨歉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來屋摇,“玉大人,你說我怎么就攤上這事幽邓∨谖拢” “怎么了?”我有些...
    開封第一講書人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵牵舵,是天一觀的道長(zhǎng)柒啤。 經(jīng)常有香客問我,道長(zhǎng)畸颅,這世上最難降的妖魔是什么担巩? 我笑而不...
    開封第一講書人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮重斑,結(jié)果婚禮上兵睛,老公的妹妹穿的比我還像新娘肯骇。我一直安慰自己窥浪,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開白布笛丙。 她就那樣靜靜地躺著漾脂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪胚鸯。 梳的紋絲不亂的頭發(fā)上骨稿,一...
    開封第一講書人閱讀 51,562評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼坦冠。 笑死形耗,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的辙浑。 我是一名探鬼主播激涤,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼判呕!你這毒婦竟也來了倦踢?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤侠草,失蹤者是張志新(化名)和其女友劉穎辱挥,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體边涕,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡晤碘,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了功蜓。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片哼蛆。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖霞赫,靈堂內(nèi)的尸體忽然破棺而出腮介,到底是詐尸還是另有隱情,我是刑警寧澤端衰,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布叠洗,位于F島的核電站,受9級(jí)特大地震影響旅东,放射性物質(zhì)發(fā)生泄漏灭抑。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一抵代、第九天 我趴在偏房一處隱蔽的房頂上張望腾节。 院中可真熱鬧,春花似錦荤牍、人聲如沸案腺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽劈榨。三九已至,卻和暖如春晦嵌,著一層夾襖步出監(jiān)牢的瞬間同辣,已是汗流浹背拷姿。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留旱函,地道東北人响巢。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像棒妨,于是被迫代替她去往敵國(guó)和親抵乓。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355

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

  • 前面花了很多篇幅介紹的JUnit和Mockito靶衍,它們都是針對(duì)Java全平臺(tái)的一些測(cè)試框架灾炭。寫到這里,咱們開始介紹...
    云飛揚(yáng)1閱讀 5,667評(píng)論 0 48
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理颅眶,服務(wù)發(fā)現(xiàn)蜈出,斷路器,智...
    卡卡羅2017閱讀 134,657評(píng)論 18 139
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,139評(píng)論 25 707
  • 前端PS技能修煉 經(jīng)過前段時(shí)間的工作涛酗,PS現(xiàn)在能夠應(yīng)用得比較熟悉了铡原,在此記錄下我PS工作區(qū)的配置,應(yīng)該也是前端開發(fā)...
    辰小右閱讀 522評(píng)論 0 0
  • 建安20年的荊州之爭(zhēng)商叹,以孫權(quán)和劉備相互妥協(xié)瓜分領(lǐng)地告終燕刻,但他們都不會(huì)因此而滿足!孫權(quán)采納呂蒙建議吞劉剖笙,在關(guān)羽去...
    張墨涵閱讀 295評(píng)論 0 2