前言
前面我们介绍了单元测试框架 JUnit 和 Mockito 的使用(详情查看:单元测试框架: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
那么,还需进行如下配置:
Sample
Activity
-
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 - 配置属性
- Configure SDK Level - 配置 SDK 版本
Robolectric 默认会以manifest
中的targetSdkVersion
运行你的测试代码。如果你想要代码运行在其他的 SDK 中,可以使使用sdk
,minSdk
和maxSdk
属性。
@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.
}
}
-
Configure Application Class - 配置
Application
类
Robolectric 默认会创建在manifest
中指定的Application
实例,如果你想要自定义另一个Application
实现,可以进行如下设置:
@Config(application = CustomApplication.class)
public class SandwichTest {
@Config(application = CustomApplicationOverride.class)
public void getSandwich_shouldReturnHamSandwich() {
}
}
-
Configure Resource and Asset Paths - 配置
Resource
和Asset
路径
Robolectric 对于 Gradle 和 Maven,有默认提供的配置,但是它也允许你自己自定义manifest
,resource
和assets
的路径。这个特定对于自定义构建系统是十分有用的,你可以自己指定这些属性:
@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 生命周期
- 获取一个初始化的
Activity
Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class).create().get();
上面的代码会创建一个MyAwesomeActivity
实例,并且经历了生命周期onCreate
。
- 测试事件在
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!
- 模拟使用
Intent
启动Activity
Intent intent = new Intent(Intent.ACTION_VIEW);
Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class).withIntent(intent).create().get();
- 模拟
Activity
异常恢复启动
Bundle savedInstanceState = new Bundle();
···
Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class)
.create()
.restoreInstanceState(savedInstanceState)
.get();
更多ActivityController
方法说明,请查看文档:ActivityController
- 控制
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 影子类,其他一些类似appcompat
和support 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 中进行查询。