单元测试框架:Robolectric

前言

前面我们介绍了单元测试框架 JUnitMockito 的使用(详情查看:单元测试框架:JUnit单元测试框架:Mockito),对于绝大多数的 Java 方法,上面两个框架的使用基本就能覆盖绝大多数测试用例编写。然而,如果我们要对 Android 代码进行测试,由于 Android 程序是跑在 Dalvik 虚拟机上的,跟普通 Java 代码跑在 JVM 上不同,因此,无法直接在 JVM 上运行 Android 程序。

Why Robolectric

由于无法直接在 JVM 上运行 Android 程序,因此平常我们对 Android 应用的测试都是通过直接将应用部署到虚拟机/真机上进行测试,而这个过程要经过打包、dexing、上传到device、安装,运行,打开界面等一系列过程,十分浪费时间,这对单元测试来说是无法忍受的。我们希望的 Android 单元测试是注重流程与实现,易于测试,时间快,耗时短,我们不想经历 dexing、打包、部署 apk 到设备这些过程,我们不需要看见界面是否出现,我们只想要测试相应的代码的逻辑与功能,因此,Robolectric 应运而生。

Robolectric 是一套单元测试框架,通过 Robolectric,我们可以在 Java 虚拟机(JVM)上对 Android 应用进行测试。

Robolectric 原理

Android 应用是运行在 Dalvik 虚拟机上的,而我们平常开发 Android app 是在 JVM 上的,因此Google 为我们开发 Android app 提供了 SDK(软件开发工具集),各个 API-level 对应的 SDK 都有相应的 android.jar 包,通过这个 androdi.jar 包,我们就可以在 JVM 上调用 Android 系统 api,进行 Android app 开发。然而,这个 android.jar 包的作用仅仅是起一个可以编译打包的功能,我们是没办法直接在 JVM 上运行 androdi.jar 的,可以在 android_sdk_home/platforms/ 查看下 android.jar 包内容,你会发现所有方法内部只有一个实现:throw RuntimeException("stub!!”);

因此,如果我们在单元测试中直接运行 Android 相关测试用例,那运行的时候就会抛出 RuntimeException("stub!!”) 异常。从这里,我们也可以知道为什么这两年 MVP,MVVM 这种框架流行的原因了,一个原因就是为了隔离 Java 层代码与 Android api 代码的耦合,方便单元测试。

这里要注意的是,当我们将写好的 Android 应用部署到虚拟机/真机时,android.jar 就会被替换成系统真正的实现,因此功能便能得到实现。

通过上面的讨论,我们可以知道,无法在 JVM 上运行 Android 代码,是因为 JVM 上 Android api 接口内部实现全部为throw RuntimeException("stub!!”);,因此,一个解决方法就是更改 android.jar 内容,真正实现 Android api 接口。而 Robolectric 的实现原理正是如此,Robolectric 为我们实现了一套 JVM 能运行的 Android api,而且是增强型的 Android api,其内部比原生 Android api 增加了更多的方法,方便我们进行调用测试。

Robolectric 优点

  • 运行 Android 单元测试,无需启动虚拟机/真机
  • 复写 Android 核心库(即 影子类 - Shadow Classes),扩展更多有用的功能。
  • 可以对 Android 多个组件进行测试,比如:
    -- Activity
    -- Service
    -- Broadcast Receiver
    可以对应用资源进行测试,比如:
    -- string.xml
    -- 应用属性配置(Configuration),比如横屏或者竖屏
    -- Styles and themes
    可以进行测试的还有:
    -- 多渠道(Multiple product flavors)

Integrate

via Gradle:

testImplementation "org.robolectric:robolectric:3.4.2"
//required  Android Studio 3.0 alpha 5
android {
  testOptions {
    unitTests {
      includeAndroidResources = true
    }
  }
} 

最新的版本可以在这里查找:Robolertic-newest-version

更多配置详情,请查看:Getting Started

如果使用的是 Android Studio,那么在运行测试用例时如果出现错误:

android.content.res.Resources$NotFoundException: String resource ID #0x7f0b001f

那么,还需进行如下配置:
在 gradle.properties 中添加内容:android.enableAapt2=false
For more detailed information,please see here

如果出现以下错误:

No such manifest file: build\intermediates\bundles\debug\AndroidManifest.xml

那么,还需进行如下配置:

Edit Configurations
Working directory

Sample

  • Activity
  1. Activity跳转测试:摘自官方 Demo
    假设布局如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/login"
        android:text="Login"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</LinearLayout>

我们希望测试当点击按钮时,启动了LoginActivity

public class WelcomeActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.welcome_activity);

        final View button = findViewById(R.id.login);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                startActivity(new Intent(WelcomeActivity.this, LoginActivity.class));
            }
        });
    }
}

我们通过按钮点击启动LoginActivity,但是由于 Robolectric 是一个单元测试框架,它并不会真正启动LoginActivity,所以我们可以通过查看 WelcomeActivity是否发出了正确的intent即可:

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class) 
public class WelcomeActivityTest {

    @Test
    public void clickingLogin_shouldStartLoginActivity() {
        WelcomeActivity activity = Robolectric.setupActivity(WelcomeActivity.class);
        activity.findViewById(R.id.login).performClick();

        Intent expectedIntent = new Intent(activity, LoginActivity.class);
        Intent actual = ShadowApplication.getInstance().getNextStartedActivity();
        assertEquals(expectedIntent.getComponent(), actual.getComponent());
    }
}

更多 [Robolertic] Sample,请查看:robolectric-samples

配置 Robolectric

  • @Config Annotation
    为一个类或者方法进行配置,可以使用注解@Config ,该注解可应用于类和方法上;方法上的注解会覆盖类上注解。
    如果你发现在多个测试用例上注解了相同内容,那么你可以创建一个基类,将注解移到基类上即可。
  @Config(sdk=JELLYBEAN_MR1,
      manifest="some/build/path/AndroidManifest.xml",
      shadows={ShadowFoo.class, ShadowBar.class})
  public class SandwichTest {
  }
  • Configurables - 配置属性
  1. Configure SDK Level - 配置 SDK 版本
    Robolectric 默认会以manifest中的targetSdkVersion运行你的测试代码。如果你想要代码运行在其他的 SDK 中,可以使使用 sdkminSdkmaxSdk属性。
@Config(sdk = { JELLY_BEAN, JELLY_BEAN_MR1 })
public class SandwichTest {

    public void getSandwich_shouldReturnHamSandwich() {
      // will run on JELLY_BEAN and JELLY_BEAN_MR1
    }

    @Config(sdk = KITKAT)
    public void onKitKat_getSandwich_shouldReturnChocolateWaferSandwich() {
      // will run on KITKAT
    }
    
    @Config(minSdk=LOLLIPOP)
    public void fromLollipopOn_getSandwich_shouldReturnTunaSandwich() {
      // will run on LOLLIPOP, M, etc.
    }
}
  1. Configure Application Class - 配置 Application
    Robolectric 默认会创建在manifest中指定的Application实例,如果你想要自定义另一个Application实现,可以进行如下设置:
@Config(application = CustomApplication.class)
public class SandwichTest {

    @Config(application = CustomApplicationOverride.class)
    public void getSandwich_shouldReturnHamSandwich() {
    }
}
  1. Configure Resource and Asset Paths - 配置 ResourceAsset路径
    Robolectric 对于 Gradle 和 Maven,有默认提供的配置,但是它也允许你自己自定义manifestresourceassets的路径。这个特定对于自定义构建系统是十分有用的,你可以自己指定这些属性:
@Config(resourceDir = "some/build/path/res")
public class SandwichTest {

    @Config(assetDir = "other/build/path/ham-sandwich/res")
    public void getSandwich_shouldReturnHamSandwich() {
    }
}

更多配置详情,请查看:Configuring Robolectric

Driving the Activity Lifecycle - 控制 Activity 生命周期

  1. 获取一个初始化的Activity
Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class).create().get();

上面的代码会创建一个MyAwesomeActivity实例,并且经历了生命周期onCreate

  1. 测试事件在onCreate未发生,在onResume发生
ActivityController controller = Robolectric.buildActivity(MyAwesomeActivity.class).create().start();
Activity activity = controller.get();
// assert that something hasn't happened
activityController.resume();
// assert it happened!
  1. 模拟使用Intent启动Activity
Intent intent = new Intent(Intent.ACTION_VIEW);
Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class).withIntent(intent).create().get();
  1. 模拟Activity异常恢复启动
Bundle savedInstanceState = new Bundle();
···
Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class)
    .create()
    .restoreInstanceState(savedInstanceState)
    .get();

更多ActivityController方法说明,请查看文档:ActivityController

  1. 控制Activity生命周期,并执行控件操作
Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class).create().start().resume().visible().get();
// now you can interacte with the views inside the Activity

上面代码的visible()表达的是Activity的视图可见,visible()调用后我们就可以对Activity视图进行操作。因为在真实的 Android app 中,Activity的视图层级是在onCreate()调用后某个时间后才附着到Activity上的,在此之前,Activity上的视图是不可见的,这意味着你不能对其视图进行点击等操作,Activity视图层级在Activity经历onPostResume()后才会附着到窗口上。与其臆测视图更新为可见,Robolectric 为开发者提供了这种自己控制视图可见性的功能。

所以,当你想操作Activity界面视图(views)时,你应当在create()后调用visible()

Using Add-On Modules - 使用附加模块

为了减小测试应用外部依赖的数量,Robolectric 影子类被切分多个附加模块。Robolectric 主模块只提供了基础 Android SDK 影子类,其他一些类似appcompatsupport library的影子类在其他的附加模块中。下表列举了附件模块影子类包名:

SDK Package Robolectric Add-On Package
com.android.support.support-v4 org.robolectric:shadows-support-v4
com.android.support.multidex org.robolectric:shadows-multidex
com.google.android.gms:play-services org.robolectric:shadows-play-services
com.google.android.maps:maps org.robolectric:shadows-maps
org.apache.httpcomponents:httpclient org.robolectric:shadows-httpclient

对于上面列举的附加模块最新版本,可以在 Maven 中进行查询。

参考

用Robolectric来做Android unit testing

Robolectric, Unit testing framework for Android

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,463评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,598评论 18 139
  • 标签(空格分隔): Android 单元测试的好处:Martin Fowler在《重构》里面还解释了为什么单元测试...
    背影杀手不太冷阅读 5,804评论 3 25
  • 参考文档 Robolectric 简介 Instrumentation 与 Roboletric 都是针对 And...
    三季人阅读 1,407评论 2 0
  • 昨晚读高原的那本《把青春唱完》,里面写道:“……那会儿也不是因为有什么事做到极致才开心的,反而是根本没有什么目的,...
    猜不中的尾声阅读 312评论 5 4