Android 的单元测试可以分为两部分:
如果使用 Local 测试,需要保证测试过程中不会调用 Android 系统 API,否则会抛出 RuntimeException 异常,因为 Local 测试是直接跑在本机 JVM 的,而之所以我们能使用 Android 系统 API,是因为编译的时候,我们依赖了一个名为 "android.jar" 的 jar 包,但是 jar 包里所有方法都是直接抛出了一个 RuntimeException,是没有任何任何实现的,这只是 Android 为了我们能通过编译提供的一个 Stub!当 APP 运行在真实的 Android 系统的时候,由于类加载机制,会加载位于 framework 的具有真正实现的类。由于我们的 Local 是直接在 PC 上运行的,所以调用这些系统 API 便会出错。
那么问题来了,我们既要使用 Local 测试,但测试过程又难免遇到调用系统 API 那怎么办?其中一个方法就是 mock objects,比如借助 Mockito,另外一种方式就是使用
, Robolectric 就是为解决这个问题而生的。它实现一套 JVM 能运行的 Android 代码,然后在 unit test 运行的时候去截取 android 相关的代码调用,然后转到他们的他们实现的 Shadow 代码去执行这个调用的过程
- Robolectric
- testCompile "org.robolectric:robolectric:3.1.4"
Robolectric 在第一次运行时,会下载一些 sdk 依赖包,每个 sdk 依赖包大概 50M,下载速度比较慢,用户可以直接在网上下载相应依赖包,放置在本地 maven 仓库地址中,默认路径为:
- C:\Users\username\.m2\repository\org\robolectric
为测试用例添加注解, 指定测试运行器为 RobolectricTestRunner。注意,这里要通过 Config 指定
,Robolectric 会通过 constants 推导出输出路径,如果不进行配置,Robolectric 可能不能找到你的 manifest、resources 和 assets 资源
- constants = BuildConfig.class
- @RunWith(RobolectricTestRunner.class)@Config(constants = BuildConfig.class) public class MainActivityTest {}
Shadow 是 Robolectric 的立足之本,如其名,作为影子,一定是变幻莫测,时有时无,且依存于本尊。Robolectric 定义了大量模拟 Android 系统类行为的 Shadow 类,当这些系统类被创建的时候,Robolectric 会查找对应的 Shadow 类并创建一个 Shadow 类与原始类关联。每当系统类的方法被调用的时候,Robolectric 会保证 Shadow 对应的方法会调用。这些 Shadow 对象,丰富了本尊的行为,能更方便的对 Android 相关的对象进行测试。
比如,我们可以借助 ShadowActivity 验证页面是否正确跳转了
- /**
- * 验证点击事件是否触发了页面跳转,验证目标页面是否预期页面
- *
- * @throws Exception
- */
- @Test public void testJump() throws Exception {
- // 默认会调用Activity的生命周期: onCreate->onStart->onResume
- MainActivity activity = Robolectric.setupActivity(MainActivity.class);
- // 触发按钮点击
- activity.findViewById(R.id.activity_main_jump).performClick();
- // 获取对应的Shadow类
- ShadowActivity shadowActivity = Shadows.shadowOf(activity);
- // 借助Shadow类获取启动下一Activity的Intent
- Intent nextIntent = shadowActivity.getNextStartedActivity();
- // 校验Intent的正确性
- assertEquals(nextIntent.getComponent().getClassName(), SecondActivity.class.getName());
- }
可以通过
定制 Robolectric 的运行时的行为。这个注解可以用来注释类和方法,,�基类,,这样,其他测试用例就可以共享这个配置了
- @Config
Robolectric 会根据 manifest 文件配置的 targetSdkVersion 选择运行测试代码的 SDK 版本,如果你想指定 sdk 来运行测试用例,可以通过下面的方式配置
- @Config(sdk = Build.VERSION_CODES.JELLY_BEAN) public class SandwichTest {@Config(sdk = Build.VERSION_CODES.KITKAT) public void getSandwich_shouldReturnHamSandwich() {}
- }
Robolectric 会根据 manifest 文件配置的 Application 配置去实例化一个 Application 类,如果你想在测试用例中重新指定,可以通过下面的方式配置
- @Config(application = CustomApplication.class) public class SandwichTest {@Config(application = CustomApplicationOverride.class) public void getSandwich_shouldReturnHamSandwich() {}
- }
Robolectric 可以让你配置 manifest、resource 和 assets 路径,可以通过下面的方式配置
- @Config(manifest = "some/build/path/AndroidManifest.xml", assetDir = "some/build/path/assetDir", resourceDir = "some/build/path/resourceDir") public class SandwichTest {@Config(manifest = "other/build/path/AndroidManifest.xml") public void getSandwich_shouldReturnHamSandwich() {}
- }
当 Robolectric 测试的时候,会尝试加载所有应用提供的资源,但如果你需要使用第三方库中提供的资源文件,你可能需要做一些特别的配置。不过如果你使用 gradle 来构建 Android 应用,这些配置就不需要做了,因为 Gradle Plugin 会在 build 的时候自动合并第三方库的资源,但如果你使用的是 Maven,那么你需要配置 libraries 变量:
- @RunWith(RobolectricTestRunner.class)@Config(libraries = {
- "build/unpacked-libraries/library1",
- "build/unpacked-libraries/library2"
- }) public class SandwichTest {}
Android 会在运行时加载特定的资源文件,如根据设备屏幕加载不同分辨率的图片资源、根据系统语言加载不同的 string.xml,在 Robolectric 测试当中,你也可以进行一个限定,让测试程序加载特定资源. 多个限定条件可以用破折号拼接在在一起。
- /**
- * 使用qualifiers加载对应的资源文件
- *
- * @throws Exception
- */
- @Config(qualifiers = "zh-rCN")@Test public void testString() throws Exception {
- final Context context = RuntimeEnvironment.application;
- assertThat(context.getString(R.string.app_name), is("单元测试Demo"));
- }
如果你嫌通过注解配置上面的东西麻烦,你也可以把以上配置放在一个 Properties 文件之中,,比如,首先创建一个配置文件 robolectric.properties:
- #放置Robolectric的配置选项: sdk = 21 manifest = some / build / path / AndroidManifest.xml assetDir = some / build / path / assetDir resourceDir = some / build / path / resourceDir
然后把 robolectric.properties 文件放到 src/test/resources 目录下,运行的时候,会自动加载里面的配置
以上设置可以通过 Gradle 进行配置,如:
- android {
- testOptions {
- unitTests.all {
- systemProperty'robolectric.dependency.repo.url',
- 'https: //local-mirror/repo'
- systemProperty'robolectric.dependency.repo.id',
- 'local'
- }
- }
- }
利用
我们可以让 Activity 执行相应的生命周期方法,如:
- ActivityController
- @Test public void testLifecycle() throws Exception {
- // 创建Activity控制器
- ActivityController controller = Robolectric.buildActivity(MainActivity.class);
- MainActivity activity = controller.get();
- assertNull(activity.getLifecycleState());
- // 调用Activity的performCreate方法
- controller.create();
- assertEquals("onCreate", activity.getLifecycleState());
- // 调用Activity的performStart方法
- controller.start();
- assertEquals("onStart", activity.getLifecycleState());
- // 调用Activity的performResume方法
- controller.resume();
- assertEquals("onResume", activity.getLifecycleState());
- // 调用Activity的performPause方法
- controller.pause();
- assertEquals("onPause", activity.getLifecycleState());
- // 调用Activity的performStop方法
- controller.stop();
- assertEquals("onStop", activity.getLifecycleState());
- // 调用Activity的performRestart方法
- controller.restart();
- // 注意此处应该是onStart,因为performRestart不仅会调用restart,还会调用onStart
- assertEquals("onStart", activity.getLifecycleState());
- // 调用Activity的performDestroy方法
- controller.destroy();
- assertEquals("onDestroy", activity.getLifecycleState());
- }
通过 ActivityController,我们可以模拟各种生命周期的变化。但是要注意,我们虽然可以随意调用 Activity 的生命周期,但是 Activity 生命周期切换有自己的检测机制,我们要遵循 Activity 的生命周期规律。比如,如果当前 Activity 并非处于 stop 状态,测试代码去调用了 controller.restart 方法,此时 Activity 是不会回调 onRestart 和 onStart 的。
除了控制生命周期,还可以在启动 Activity 的时候传递 Intent:
- /**
- * 启动Activity的时候传递Intent
- *
- * @throws Exception
- */
- @Test public void testStartActivityWithIntent() throws Exception {
- Intent intent = new Intent();
- intent.putExtra("test", "HelloWorld");
- Activity activity = Robolectric.buildActivity(MainActivity.class).withIntent(intent).create().get();
- assertEquals("HelloWorld", activity.getIntent().getExtras().getString("test"));
- }
onRestoreInstanceState 回调中传递 Bundle:
- /**
- * savedInstanceState会在onRestoreInstanceState回调中传递给Activity
- *
- * @throws Exception
- */
- @Test public void testSavedInstanceState() throws Exception {
- Bundle savedInstanceState = new Bundle();
- Robolectric.buildActivity(MainActivity.class).create().restoreInstanceState(savedInstanceState).get();
- // verify something
- }
在真实环境下,视图是在 onCreate 之后的某一时刻在 attach 到 Window 上的,在此之前,View 是处于不可操作状态的,你不能点击它。在 Activity 的 onPostResume 方法调用之后,View 才会 attach 到 Window 之中。但是,在 Robolectric 之中,我们可以用控制器的
方法使得 View 变为可见,变为可见之后,就可以模拟点击事件了
- visible
- @Test public void testVisible() throws Exception {
- ActivityController controller = Robolectric.buildActivity(MainActivity.class);
- MainActivity activity = controller.get();
- // 调用Activity的performCreate并且设置视图visible
- controller.create().visible();
- // 触发点击
- activity.findViewById(R.id.activity_main_button1).performClick();
- // 验证
- assertEquals(shadowOf(activity).getNextStartedActivity().getComponent().getClassName(), SecondActivity.class.getName());
- }
为了减少依赖包的大小,Robolectric 的 shadows 类成了好几部分:
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 |
用户可以根据自身需求添加以下依赖包,如
- dependencies {......testCompile'org.robolectric: robolectric: 3.1.4'testCompile'org.robolectric: shadows - support - v4: 3.1.4'testCompile'org.robolectric: shadows - multidex: 3.1.4'testCompile'org.robolectric: shadows - play - services: 3.1.4'testCompile'org.robolectric: shadows - maps: 3.1.4'testCompile'org.robolectric: shadows - httpclient: 3.1.4'
- }
,在 Shadow 类中定义 public void 类型的名为
- 有参构造方法
的方法,且方法参数与原始类的构造方法参数一直
- __constructor__
下面我们来创建 RobolectricBean 的 Shadow 类
原始类:
- public class RobolectricBean {
- String name;
- int color;
- public RobolectricBean(String name) {
- this.name = name;
- }
- public String getName() {
- return name;
- }
- public int getColor() {
- return color;
- }
- public void setColor(int color) {
- this.color = color;
- }
- }
Shadow 类:
- /**
- * RobolectricBean}的影子类
- *
- * @author HansChen
- */
- @Implements(RobolectricBean.class) public class ShadowRobolectricBean {
- /**
- * ,但注意,,依然会调用Shadow类的方法,而不是原始类的方法
- * 只能用来访问原始类的field
- */
- @RealObject RobolectricBean realBean;
- /**
- * 需要一个无参构造方法
- */
- public ShadowRobolectricBean() {}
- /**
- * 对应原始类的构造方法
- *
- * @param name 对应原始类构造方法的传入参数
- */
- public void __constructor__(String name) {
- realBean.name = name;
- }
- /**
- * 原始对象的方法被调用的时候,Robolectric会根据方法签名查找对应的Shadow方法并调用
- */
- @Implementation public String getName() {
- return "Hello, I ma shadow of RobolectricBean: " + realBean.name;
- }@Implementation public int getColor() {
- return realBean.color;
- }@Implementation public void setColor(int color) {
- realBean.color = color;
- }
- }
Shadow 类中可以定义一个原始类的成员变量,,这样,Shadow 类就能访问原始类的 field 了,但是注意,,依然会调用 Shadow 类的方法,而不是原始类的方法,只能用它来访问原始类的 field。
- @Implements(Point.class) public class ShadowPoint {@RealObject private Point realPoint;...public void __constructor__(int x, int y) {
- realPoint.x = x;
- realPoint.y = y;
- }
- }
在 Config 注解中添加
参数,指定对应的 Shadow 生效
- shadows
- @RunWith(RobolectricTestRunner.class)@Config(shadows = ShadowRobolectricBean.class) public class RobolectricBeanTest {......
- }
注意,自定义的 Shadow 类不能通过
获取,需要用
- Shadows.shadowOf()
来获取,获取之后进行类型转换:
- ShadowExtractor.extract()
- ShadowRobolectricBean shadowBean = (ShadowRobolectricBean) ShadowExtractor.extract(bean);
- /**
- * 验证点击事件是否触发了页面跳转,验证目标页面是否预期页面
- *
- * @throws Exception
- */
- @Test public void testJump() throws Exception {
- // 默认会调用Activity的生命周期: onCreate->onStart->onResume
- MainActivity activity = Robolectric.setupActivity(MainActivity.class);
- // 触发按钮点击
- activity.findViewById(R.id.activity_main_jump).performClick();
- // 获取对应的Shadow类
- ShadowActivity shadowActivity = Shadows.shadowOf(activity);
- // 借助Shadow类获取启动下一Activity的Intent
- Intent nextIntent = shadowActivity.getNextStartedActivity();
- // 校验Intent的正确性
- assertEquals(nextIntent.getComponent().getClassName(), SecondActivity.class.getName());
- }
- /**
- * 验证UI组件状态
- *
- * @throws Exception
- */
- @Test public void testCheckBoxState() throws Exception {
- MainActivity activity = Robolectric.setupActivity(MainActivity.class);
- CheckBox checkBox = (CheckBox) activity.findViewById(R.id.activity_main_check_box);
- // 验证CheckBox初始状态
- assertFalse(checkBox.isChecked());
- // 点击按钮反转CheckBox状态
- activity.findViewById(R.id.activity_main_switch_check_box).performClick();
- // 验证状态是否正确
- assertTrue(checkBox.isChecked());
- // 点击按钮反转CheckBox状态
- activity.findViewById(R.id.activity_main_switch_check_box).performClick();
- // 验证状态是否正确
- assertFalse(checkBox.isChecked());
- }
- /**
- * 验证Dialog是否正确弹出
- *
- * @throws Exception
- */
- @Test public void testDialog() throws Exception {
- MainActivity activity = Robolectric.setupActivity(MainActivity.class);
- AlertDialog dialog = ShadowAlertDialog.getLatestAlertDialog();
- // 判断Dialog尚未弹出
- assertNull(dialog);
- activity.findViewById(R.id.activity_main_show_dialog).performClick();
- dialog = ShadowAlertDialog.getLatestAlertDialog();
- // 判断Dialog已经弹出
- assertNotNull(dialog);
- // 获取Shadow类进行验证
- ShadowAlertDialog shadowDialog = shadowOf(dialog);
- assertEquals("AlertDialog", shadowDialog.getTitle());
- assertEquals("Oops, now you see me ~", shadowDialog.getMessage());
- }
- /**
- * 验证Toast是否正确弹出
- *
- * @throws Exception
- */
- @Test public void testToast() throws Exception {
- MainActivity activity = Robolectric.setupActivity(MainActivity.class);
- Toast toast = ShadowToast.getLatestToast();
- // 判断Toast尚未弹出
- assertNull(toast);
- activity.findViewById(R.id.activity_main_show_toast).performClick();
- toast = ShadowToast.getLatestToast();
- // 判断Toast已经弹出
- assertNotNull(toast);
- // 获取Shadow类进行验证
- ShadowToast shadowToast = shadowOf(toast);
- assertEquals(Toast.LENGTH_SHORT, shadowToast.getDuration());
- assertEquals("oops", ShadowToast.getTextOfLatestToast());
- }
- @RunWith(RobolectricTestRunner.class)@Config(constants = BuildConfig.class, application = CustomApplication.class) public class MyFragmentTest {
- private MyFragment myFragment;@Before public void setUp() throws Exception {
- myFragment = new MyFragment();
- // 把Fragment添加到Activity中
- FragmentTestUtil.startFragment(myFragment);
- }@Test public void testFragment() throws Exception {
- assertNotNull(myFragment.getView());
- }
- }
首先看下广播接收器:
- public class MyReceiver extends BroadcastReceiver {@Override public void onReceive(Context context, Intent intent) {
- // do something
- }
- }
广播的测试点可以包含两个方面
- @RunWith(RobolectricTestRunner.class)@Config(constants = BuildConfig.class, application = CustomApplication.class) public class MyReceiverTest {@Test public void restRegister() throws Exception {
- ShadowApplication shadowApplication = ShadowApplication.getInstance();
- String action = "ut.cn.unittestdemo.receiver";
- Intent intent = new Intent(action);
- // 验证是否注册了相应的Receiver
- assertTrue(shadowApplication.hasReceiverForIntent(intent));
- }@Test public void restReceive() throws Exception {
- String action = "ut.cn.unittestdemo.receiver";
- Intent intent = new Intent(action);
- intent.putExtra("EXTRA_USERNAME", "HansChen");
- MyReceiver myReceiver = new MyReceiver();
- myReceiver.onReceive(RuntimeEnvironment.application, intent);
- // verify something
- }
- }
Service 和 Activity 一样,都有生命周期,Robolectric 也提供了 Service 的生命周期控制器,使用方式和 Activity 类似,这里就不做详细解释了
- @RunWith(RobolectricTestRunner.class)@Config(constants = BuildConfig.class, application = CustomApplication.class) public class TestServiceTest {
- private ServiceController controller;
- private TestService testService;@Before public void setUp() throws Exception {
- controller = Robolectric.buildService(TestService.class);
- testService = controller.get();
- }
- /**
- * 控制Service生命周期进行验证
- *
- * @throws Exception
- */
- @Test public void testLifecycle() throws Exception {
- controller.create();
- // verify something
- controller.startCommand(0, 0);
- // verify something
- controller.bind();
- // verify something
- controller.unbind();
- // verify something
- controller.destroy();
- // verify something
- }
- }
来源: http://www.bubuko.com/infodetail-1868723.html