介紹
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)我們通過 Robolectric
的 setupActivity
構(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)造 SdkEnvironment
中 InstrumentingClassLoader
的身影拴签,細(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 一起交流。