Android单元测试-Robolectric 浅析

介绍

Robolectric 测试框架针对 Android 的组件(包含各种View)进行了统一的 Shadow,使得我们不再依赖模拟器或真机,直接就单元测试就可方便地测试我们的 UI。

引入

testCompile "org.robolectric:robolectric:3.1.1"

使用

1.通用 Demo 示例

这里先来一个简单的 Demo, 也是我们经常使用的形式:

@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 类对应的关系:

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 类
@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_ 来编写。

  • 测试调用
@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 类中,进行添加的,可看代码:

@Implementation
public void show() {
 shadowOf(RuntimeEnvironment.application).getShownToasts().add(toast);
}

因为 ShadowToast 类中也对 show 方法做了实现,但是其却被我们自定义实现给替换掉了。所以我们在自定义 Shadow 实现的时候,需要对这一点谨慎一二。

另外,我们也有在自定义 Shadow 的时候,需要持有真实类的引用,可以直接使用 RealObject 注解,就像 ShadowToast 一样:

@Implements(Toast.class)
public class ShadowToast {
   
   // 省略

  @RealObject Toast toast;
}

浅析

相信大家也是同我一样会对这里的 Shadow 实现颇感兴趣的。问题是 Shadow 类是如何跟真实的类挂上关系的?我们在针对真实类方法的调用,最后却调用的是 Shadow 类里面的方法。

以第一个 Demo 中的 ShadowActivity 的获取为例,查看 shadowOf 方法:

public static ShadowActivity shadowOf(Activity actual) {
 return (ShadowActivity) ShadowExtractor.extract(actual);
}

进而再看 ShadowExtractor

public class ShadowExtractor {
  public static Object extract(Object instance) {
    return ((ShadowedObject) instance).$$robo$getData();
  }
}

而其中的 ShadowedObject 就是一个很简单的接口:

public interface ShadowedObject {
  Object $$robo$getData();
}

由此可知,我们的 Activity 对象 actual 其实已经实现了 ShadowedObject 接口。这个就比较吊了啊,这里代码查看到头,再追溯 Activity 是如何构造的,发现并无什么特别的地方。那最后只剩 @RunWith 注解的参数 RobolectricTestRunner 类了,在 runChild 方法中,发现构造 SdkEnvironmentInstrumentingClassLoader 的身影,细看这个类,发现应该就是它完成了我们所需要的功能。

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

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

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

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

参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容

  • 前面花了很多篇幅介绍的JUnit和Mockito,它们都是针对Java全平台的一些测试框架。写到这里,咱们开始介绍...
    云飞扬1阅读 5,657评论 0 48
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,637评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,856评论 25 707
  • 前端PS技能修炼 经过前段时间的工作,PS现在能够应用得比较熟悉了,在此记录下我PS工作区的配置,应该也是前端开发...
    辰小右阅读 520评论 0 0
  • 建安20年的荆州之争,以孙权和刘备相互妥协瓜分领地告终,但他们都不会因此而满足!孙权采纳吕蒙建议吞刘,在关羽去...
    张墨涵阅读 286评论 0 2