介绍
Robolectric 测试框架针对 Android 的组件(包含各种View)进行了统一的 Shadow
,使得我们不再依赖模拟器或真机,直接就单元测试就可方便地测试我们的 UI。
引入
1
| testCompile "org.robolectric:robolectric:3.1.1"
|
使用
1.通用 Demo 示例
这里先来一个简单的 Demo, 也是我们经常使用的形式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| @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));
}
}
|
在真实的 TestMainActivity
中,存在一个按钮和一个文本框,当点击按钮之后,将文本框的内容修改为 “hello”。当我们通过 Robolectric
的 setupActivity
构造出来一个 Activity
之后,对其进行操作并验证,完全符合我们的预期结果。
另外,在上面的示例中,针对 Shadow
的使用,我们通过真实的 startActivity
方法启动下一个 Activity
。若此时,我们需要验证其是否启动成功,就可以使用其对应的 ShadowActivity
。在拿到 ShadowActivity
之后,通过获取其 getNextStartedActivity
,就可验证其是否启动成功。
2.Custom Shadow 的使用
初次接触这个 Shadow
可能有些困惑,我们在 Robolectric 给我们提供的 Shadows
类中,可以发现其已经有很多的 Shadow
实现,其以一个 map 的格式存储真实类跟 shadow 类对应的关系:
1
2
3
4
5
6
7
8
9
10
11
| 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");
**** 省略
}
|
这里,大概就可以获悉其的实现方法,通过 Shadow
类来替换其对应的真实方法的实现,最终达到的目的就会使我们的测试脱离一些底层的具体实现,来达到我们最快测试的目的。
若是大家感兴趣的话,可以具体查看相应组件类的 Shadow
实现。当然,这里我们也可以自定义 Shadow
,来满足定制化的需求,这里来个很简单的实现:
+ 定义 Shadow 类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| @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 为例,只对其 show 方法做以实现,当调用了 show
方法之后,我们将一静态变量 mIsShown
标记为 true,通过 isToastShowInvoked
方法来进行判断其是否调用。
需要注意的三点:@Implements 注解指定需要对哪个类进行 shadow;@Implementation 指定需要对哪个方法进行替换;构造器需要通过 __constructor__ 来编写。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| @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 的点击,来随意显示一个 Toast ,我们是可以发现自定义 CustomShadowToast
的静态变量确实是调用了。
不过第二个 assertThat
方法对显示的 toast
数目做判断,却发现个数为零。这 shownToasts
数目的改变,是在 ShadowToast
类中,进行添加的,可看代码:
1
2
3
4
| @Implementation
public void show() {
shadowOf(RuntimeEnvironment.application).getShownToasts().add(toast);
}
|
因为 ShadowToast
类中也对 show
方法做了实现,但是其却被我们自定义实现给替换掉了。所以我们在自定义 Shadow
实现的时候,需要对这一点谨慎一二。
另外,我们也有在自定义 Shadow
的时候,需要持有真实类的引用,可以直接使用 RealObject
注解,就像 ShadowToast
一样:
1
2
3
4
5
6
7
| @Implements(Toast.class)
public class ShadowToast {
// 省略
@RealObject Toast toast;
}
|
浅析
相信大家也是同我一样会对这里的 Shadow
实现颇感兴趣的。问题是 Shadow
类是如何跟真实的类挂上关系的?我们在针对真实类方法的调用,最后却调用的是 Shadow
类里面的方法。
以第一个 Demo 中的 ShadowActivity
的获取为例,查看 shadowOf
方法:
1
2
3
| public static ShadowActivity shadowOf(Activity actual) {
return (ShadowActivity) ShadowExtractor.extract(actual);
}
|
进而再看 ShadowExtractor
:
1
2
3
4
5
| public class ShadowExtractor {
public static Object extract(Object instance) {
return ((ShadowedObject) instance).$$robo$getData();
}
}
|
而其中的 ShadowedObject
就是一个很简单的接口:
1
2
3
| public interface ShadowedObject {
Object $$robo$getData();
}
|
由此可知,我们的 Activity 对象 actual
其实已经实现了 ShadowedObject
接口。这个就比较吊了啊,这里代码查看到头,再追溯 Activity
是如何构造的,发现并无什么特别的地方。那最后只剩 @RunWith
注解的参数 RobolectricTestRunner
类了,在 runChild
方法中,发现构造 SdkEnvironment
中 InstrumentingClassLoader
的身影,细看这个类,发现应该就是它完成了我们所需要的功能。
首先,它继承了 ClassLoader
,它在 loadClass
中进行了重写,对由需要由自己进行特殊加载的类,执行 findClass
的方法,否则用父类的 loadClass
方法。
在 findClass
中,其使用了 ASM 这个字节码修改库,来对我们需要修改的类的字节码做修改,使其与我们的 shadow
相绑定。最可证明的就是其中的这段代码:
1
| classNode.interfaces.add(Type.getInternalName(ShadowedObject.class));
|
通过 ASM 的 ClassNode
对象添加了 ShadowedObject
的接口,与我们之前看到的相吻合。但是类方法是如何替换的,这里的代码就看的是一头雾水了。这里先留一个坑,以后理解了 Java 的字节码,再来填这个坑。若是有小伙伴对这里也有兴趣,可加 QQ 群:289926871 一起交流。
参考资料