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>