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