Robolectric 框架是一款可以在JVM 上运行 Android 相关代码的框架,在 Android 单元测试总起到至关重要的作用。它既有减少编译、测试用例运行的时间等优点。

Robolectric 框架简介

Android 测试用例编写的过程中,和 Android 框架进行交互的单元是测试难点所在,也是主要耗费时间的地方,针对这类问题,在 Robolectric 框架出现之前主要有两种解法:

  • 编写 Android JUnit Test,这类测试用例的优点是不用对框架相关的代码进行特殊处理,缺点是每次运行都需要编译被测试代码 apk 和测试代码 apk,比较耗时;
  • 对 Android 框架相关的类、方法等进行 mock,此方法可以减少编译及运行时间,但是对大量框架相关代码的mock 对测试用例编写带来了一定的难度及复杂度;

幸运的是 Robolectric 框架的出现解决了这一系列问题。Robolectric可以在 JVM 上运行 Android 相关的代码,不需要借助手机就可以对 Activity、Service 等四大组件、资源等进行测试,给 Android 测试带来了很大的便利。

Robolectric 框架主要有以下优点:

  • 框架对 Android.jar 包中几乎所有的类都进行了映射(Shadow),即对于一个类X,则有一个其影子ShadowX,比如,Log类对应的影子为ShadowLog,测试用例过程中可以想操作原始类一样操作其映射。同时Robolectric 还提供了自定义影子的方法,这点后面会说到。

    除此之外,Robolectric 框架还对资源、Native 方法都进行了特殊的处理,使之能在 JVM 上运行。

  • 在虚拟机和真实设备之外运行测试用例,正如前面说到的一样,Robolectric 对框架的代码做了重新实现,使之可以脱离手机或者虚拟机运行。

  • 不需要去 mock 框架相关代码,当然,必要的情况下可以配合 mock 或者 powermock 等 mock 框架进行更全面的测试(后续文章会降到 Robolectric 和 PowerMock 框架搭配进行单元测试)。

Gralde 配置

使用 Robolectric 框架需要引入其对应的依赖包,并做其它的一些简单配置,如下:

android {
...
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
...
dependencies {
testImplementation 'org.robolectric:robolectric:3.8'
}

Robolectric 配置

Robolectric 框架的配置主要分为两个:

  • Runner 指定:

    @RunWith(RobolectricTestRunner.class)
    public class RobolectricDemoTest{
    }
  • 利用 @Config指定一些其它的配置,可以指定的配置主要有以下一些:

    配置属性 描述
    sdk / minSdk / maxSdk 指定测试用例所工作 / 最小 / 最大的 sdk 版本
    manifest 指定 Android manifest 文件,该文件中的资源、assert 文件都将被加载。
    constants 指定由 Gradle 编译生成的 BuildConfig文件,默认为 Void.class
    packageName R.class文件所在的包名,如果在 gradle 文件的 productflavor 对包名进行了改变,则需要指定该设置
    application 指定测试用例所使用的 Application类,次指定将会覆盖 AndroidManifest中指定的 Application 类
    qualifiers 指定资源文件所使用的语言、横竖屏等,在多语言测试时很有用,方法和类级别注解都可以
    resourceDir 指定加载资源所使用的文件夹,默认为res
    assetDir 指定加载 assert 文件所使用的文件夹,默认为assets
    shadows 指定自定义 shadow 文件,可以同时指定多个
    instrumentedPackages 已经设备化的包名列表
    libraries 工程所依赖的 library 文件夹,可以同时指定多个,默认为{}

    例如:

    @Config(manifest = "AndroidManifest.xml",
    // sdk = 27,
    minSdk = 26,
    maxSdk = 28,
    shadows = CustomShadow.class,
    libraries = {
    "path/to/library1",
    "path/to/library2"
    })
    @RunWith(RobolectricTestRunner.class)
    public class RobolectricTest {
    @Test
    @Config(qualifiers = "zh-CN"
    application = CustomApplication.class)
    public void name() {
    }
    }

    minSdkmaxSdk不能同时和sdk同时出现

  • gradle 文件中配置系统属性

    android {
    testOptions {
    unitTests.all {
    systemProperty 'robolectric.dependency.repo.url', 'https://local-mirror/repo'
    systemProperty 'robolectric.dependency.repo.id', 'local'
    }
    }
    }

自定义 Runner 加快下载 jar 包的速度

以 RobolectricTestRunner 作为注解的测试用例在运行之前会下载 android-all-x.x.x 相关文件,但是这个网站下载速度是能是龟速,有时候还会出现下载失败的问题。这种情况可以通过自定义 Runner,添加自定义的 maven 地址,例如阿里云等。当然也可以离线下载到<USER_HOME>/.m2/repository/org/robolectric/android-all/xxx/目录下,xxx为 jar 包的版本号。

RoboSettings 类中定义两个静态变量mavenRepositoryIdmavenRepositoryUrl,分别为 maven 仓库的 id 和 url,Robolectric 是通过读取这两个的值来获取 maven 仓库地址,因为只需要在自定义 Runner 中分别制定这两个变量值:

public class CustomRobolectricRunner extends RobolectricTestRunner {
public CustomRobolectricRunner(Class<?> testClass) throws InitializationError {
super(testClass);
// 设置 maven 仓库 id 和 url
RoboSettings.setMavenRepositoryId("aliyun");
RoboSettings.setMavenRepositoryUrl("http://maven.aliyun.com/nexus/content/groups/public/");
}
}

如果要修改全局配置,则需要在自定义 Runner 中重写buildGlobalConfig方法。

测试 Android 组件

Robolectric 框架不仅可以测试 Activity 等 Android 四大组件,也可以测试例如 Dialog、Toast等,也可以测试控件的状态。

Activity测试

Activity 测试主要包括 Activity 的创建、生命周期以及 Activity 的跳转等,下面分别进行举例。

Activity 创建测试

Robolectric 框架提供了ActivityController 类来操作 Activity,不仅可以创建 Activity,还可以对 Activity 的周期函数进行操作。

@Test
public void activitySetupTest() {
RobolectricDemoActivity activity = Robolectric.buildActivity(RobolectricDemoActivity.class)
.create()
.get();

assertNotNull(activity);
}

Activity 生命周期测试

@Test
public void activityLifeCycleTest() {
ActivityController<RobolectricDemoActivity> controller = Robolectric.buildActivity(RobolectricDemoActivity.class);

controller.create(); // 相当于执行完 onCreate 周期函数
// 测试 onCreate 执行之后的状态

controller.resume(); // 相当于执行完 onResume 周期函数
// 测试 onResume 执行之后的状态

controller.pause(); // 相当于执行完 onPause 周期函数
// 测试 onPause 执行之后的状态

controller.stop(); // 相当于执行完 onStop 周期函数
// 测试 onStop 执行之后的状态

controller.destroy(); // 相当于执行完 onDestroy 周期函数
// 测试 onDestroy 执行之后的状态
}

Activity 跳转测试

下面是测试点击一个按钮起动 Activity 的用例

@Test
public void jumpActivityTest() {
RobolectricDemoActivity activity = Robolectric.buildActivity(RobolectricDemoActivity.class)
.create().get();
Button button = activity.findViewById(R.id.aty_jump);

Intent exceptIntent = new Intent(activity, LoginActivity.class);

button.performClick(); // 模拟点击按键
Intent actualIntent = ShadowApplication.getInstance().getNextStartedActivity();

assertEquals(exceptIntent.getComponent(), actualIntent.getComponent());
}

Dialog 测试

测试点击按钮弹出 Dialog 的测试代码如下:

@Test
public void dialogTest() {
RobolectricDemoActivity activity = Robolectric.buildActivity(RobolectricDemoActivity.class)
.create().get();
Button button = activity.findViewById(R.id.show_dialog);
button.performClick();
ShadowDialog dialog = ShadowApplication.getInstance().getLatestDialog(); // 获取最近弹出的对话框

assertNotNull(dialog);
assertEquals("DialogTest", dialog.getTitle());
}

Toast 测试

@Test
public void toastTest() {
RobolectricDemoActivity activity = Robolectric.buildActivity(RobolectricDemoActivity.class)
.create().get();
Button button = activity.findViewById(R.id.toast);
button.performClick();
Toast toast = ShadowToast.getLatestToast(); // 获取最近弹出的 toast

assertNotNull(toast);
assertEquals("ToastTest", ShadowToast.getTextOfLatestToast());
}

控件状态测试

在 Activity 起动获取到控件之后,可以对控件的状态进行验证,以下是一个验证是否可用的例子:

@Test
public void widgetTest() {
RobolectricDemoActivity activity = Robolectric.buildActivity(RobolectricDemoActivity.class)
.create().get();
Button button = activity.findViewById(R.id.toast);

assertNotNull(button);
assertTrue(button.isEnabled());
assertEquals("Toast", button.getText());
}

BroadCastReceiver 测试

广播接收器的测试主要分为两类:

  • 是否注册该广播;
  • 接收到广播之后逻辑是否正常;

以下是一个接收弹出 Toast 的一个广播接收器的测试用例:

@Test
public void receiverTest() {
Intent intent = new Intent("cc.istarx.MyReceiver");
intent.putExtra("receiver_test", "Receiver Test");

MyReceiver receiver = new MyReceiver();
// 验证是否注册
assertTrue(ShadowApplication.getInstance().hasReceiverForIntent(intent));

// 验证逻辑是否正确
ShadowApplication.getInstance().sendBroadcast(intent);
assertEquals("Receiver Test", ShadowToast.getTextOfLatestToast());
}

Service 测试

以自定义 IntentService 为例,service 主体代码及测试代码如下:

public class MyService extends IntentService {
@Override
protected void onHandleIntent(@Nullable Intent intent) {
// do something here
}
}

// 测试代码
@Test
public void serviceTest() {
Intent intent = new Intent();

MyService service = new MyService("test");
service.onHandleIntent(intent);

// verify logical here
}

资源测试

资源测是通过@Config(qualifiers = "")知道不同语言、分辨率或者横竖屏,对不同的资源进行测试:

/**
* res/values/strings.xml
* <string name="login_activity_res">This is Login Activity</string>
*
* res/values-zh-rCN/strings.xml
* <string name="login_activity_res">This is Login Activity with zh-rCN</string>
*/
@Test
@Config(qualifiers = "zh-rCN") // 指定中文语言
public void resourcesTest() {
Context context = RuntimeEnvironment.application;
String str = context.getResources().getString(R.string.login_activity_res);

assertEquals("This is Login Activity with zh-rCN", str);
}

Looper 测试

Looper 测试主要是测试代码中使用 handler处理消息产生延迟的情况。假如使用 hanler 发送一个延时的消息到消息队列,但是在测试用例验证的过程中次消息不一定被执行到,因此后续的验证会产生一定的错误,这种情况下需要利用Looper 单独去测试 handler 所发消息的逻辑。

// handle 代码如下:
public static void useHandler(){
Handler handler = new Handler();
Message message = new Message();
message.what =1;
handler.postDelayed(new Runnable() {
@Override
public void run() {
// do something here;
Log.d("Looper", "Looper test.");
}
}, 100);
}

当测试代码执行useHandler方法时,run 内部的逻辑不会执行,Log 不会打印,因此测试用例代码需要单独运行消息队列中的消息:

@Test
public void looperTest() {
RobolectricLooperDemo.useHandle();

ShadowLooper looper = ShadowLooper.getShadowMainLooper();
looper.runOneTask();

// verify
}

此时 run 方法将会得到执行。

自定义 Shadow

Shadow 作为 Robolectric 框架的核心所在,是该框架的重中之重。Shadow 意为影子,Robolectric 框架对 android.jar包中大部分类都做了影子,例如Activity的影子为ShadowActivityView 的影子为ShadowView。虽然 Robolectric 做了很多影子,但是不一定满足我们项目测试所需。Robolectric 还支持自定义影子,并用@config注解指定即可。

假如有类文件如下:

public class Demo {

public String getStr() {
return "Demo";
}
}

自定义 Shadow 的过程如下:

  • @Implements(Demo.class)注解自定义的 Shadow 类
  • 为自定义 Shadow 提供了一个 public 的构造函数;
  • 对原始类的方法以@Implementation,并做自定义实现;
  • 在使用的测试类或者测试方法上以@Config(shadows = ShadowDemo.class);

接下来在执行本体类方法的时候就会由 Robolectric 框架转到执行自定义 Shadow 内部的方法。另外,自定义 Shadow 不仅可以实现本体的方法,还可以添加自定义方法作为本体的一种扩展。上述类的自定义 Shadow 如下:

@Implements(Demo.class)
public class ShadowDemo {
public ShadowDemo(){
}

@Implementation
public String getStr() {
return "Shadow Demo implementation";
}
}

测试代码如下:

@Config(shadows = ShadowDemo.class)
public void customShadowTest() {
Demo demo = new Demo();
assertEquals("Shadow Demo implementation", demo.getStr());
}

在知道本体对象的情况下使用如下方法去获取对应的影子对象:

Demo demo = new Demo();
ShadowDemo shadowDemo = Shadows.shadowOf(demo);

小结

Robolectric 框架在 Android 测试中有着很大的便利性,可以快速编写并运行测试用例。在我写测试用例的过程中,Robolectric 框架相关的占了很大的比例。上述所讲到的只是 Robolectric 框架很小的一部分,其它用法还需要阅读文档或者看源代码进行挖掘。

本文所有的示示例代码:Robolectric Demo