alighters

程序、写作、人生

Android单元测试-Robolectric 浅析

| Comments

介绍

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”。当我们通过 RobolectricsetupActivity 构造出来一个 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 方法中,发现构造 SdkEnvironmentInstrumentingClassLoader 的身影,细看这个类,发现应该就是它完成了我们所需要的功能。

首先,它继承了 ClassLoader ,它在 loadClass 中进行了重写,对由需要由自己进行特殊加载的类,执行 findClass 的方法,否则用父类的 loadClass 方法。

findClass 中,其使用了 ASM 这个字节码修改库,来对我们需要修改的类的字节码做修改,使其与我们的 shadow 相绑定。最可证明的就是其中的这段代码:

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

通过 ASM 的 ClassNode 对象添加了 ShadowedObject 的接口,与我们之前看到的相吻合。但是类方法是如何替换的,这里的代码就看的是一头雾水了。这里先留一个坑,以后理解了 Java 的字节码,再来填这个坑。若是有小伙伴对这里也有兴趣,可加 QQ 群:289926871 一起交流。

参考资料

版权归作者所有,转载请注明原文链接:/blog/2016/09/08/unit-test-robolectric/

给 Ta 个打赏吧...

Comments