前面花了很多篇幅介绍的JUnit和Mockito,它们都是针对Java全平台的一些测试框架。写到这里,咱们开始介绍一个专门针对Android平台的单元测试框架Robolectric。
Robolectrie官网:http://robolectric.org
7.1 关于Robolectric
Android程序员都知道,在Android模拟器或者真机设备上运行测试是很慢的。执行一次测试需要编译、部署、启动app等一系列步骤,往往需要花几分钟或者更久的时间。
Robolectric框架就可解决这个问题,它实现了一套JVM能运行的Android SDK,从而能够脱离Android环境进行测试,将原来运行一次测试的时间从几分钟缩短到几秒钟。这简直是Android程序员的福音,是不是很赞。
7.2 测试环境搭建
- 在build.gradle中添加以下依赖:
- 基础配置
//版本号根据自己的需要进行修改
testCompile "org.robolectric:robolectric:3.3.2"
- 如果项目里有用到support-v4包,需要如下配置
testCompile 'org.robolectric:shadows-support-v4:3.3.2'
- 关于MultiDex
Robolectric是运行在JVM上的,所以也就没有“MultiDex”这回事,如果我们的工程里使用了它,还需如下配置:
testCompile 'org.robolectric:shadows-multidex:3.3.2'
它hook了MultiDex的实现,让它在install的时候啥也不干。
- 通过注解设置测试类的test runner
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 23)
public class RobolectricSampleActivityTest {
//必须指定test runner为RobolectricTestRunner
//通过@Config注解来配置运行参数
}
- Mac及Linux用户必须注意如下配置
在Mac以及Linux上,第一次使用时,经常会出现一个很诡异的问题,会告诉我们如下错误:
No such manifest file: build/intermediates/bundles/debug/AndroidManifest.xml
这个时候我们只需点击“Edit Configurations...” -> “Defaults” -> “JUnit”,修改“Working directory”的值为“MODULE_DIR”,就可解决该问题。
其官网上也有特意说明:http://robolectric.org/getting-started/
- Android Studio 3.0注意事项
如果Android Studio是3.0及以上版本的,需要加上以下配置,否则运行不起来,会一直报错。
android {
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
7.3 第一个测试案例
场景:我们有一个Activity,里面有一个Button,点击该Button跳转到另外一个Activity。
测试目的:验证用户点击该Button后,确实是跳转到了我们指定的Activity。
具体的布局文件以及Activity代码如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<Button
android:id="@+id/btn_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="unit test"
/>
</RelativeLayout>
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//点击Button后,跳转到RobolectricSampleActivity这个界面
findViewById(R.id.btn_main).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, RobolectricSampleActivity.class);
startActivity(intent);
}
});
}
}
测试代码如下:
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class)
public class MainActivityTest {
@Test
public void testClickBtnShouldStartSampleActivity() {
MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);
mainActivity.findViewById(R.id.btn_main).performClick();
Intent expectedIntent = new Intent(mainActivity, RobolectricSampleActivity.class);
Intent actualIntent = shadowOf(mainActivity).getNextStartedActivity();
Assert.assertEquals(expectedIntent.getComponent(), actualIntent.getComponent());
}
}
第一次运行需要比较长的时间,主要是从远程仓库下载robolectric所需的依赖库到本地,请耐心等待即可。
7.4 @Config配置
可以通过@Config注解来配置Robolectric运行时的行为。这个注解可以用来注释类和方法,如果类和方法同时使用了@Config,那么方法的设置会覆盖类的设置。如果你有很多测试类都采用同样的配置,那么你可以创建一个基类,通过@Config注解配置该基类,那么其他子类都能共享该配置。
7.4.1 配置constants
@Config(constants = BuildConfig.class)
使用@RunWith(RobolectricTestRunner.class)时,必须要指定@Config(constants = BuildConfig.class),这样它会从build/intermediates/目录下找到manifest、assets、resource等目录并加载相应的资源。
如上图所示,指定该配置后,Robolectric才会加载图中所示文件夹中对应的资源,否则就无法加载manifest、assets、resource等资源,测试也跑不起来。
7.4.2 配置SDK版本
@Config(sdk = 23)
7.4.3 配置Application
@Config(application = BaseApplication.class)
前面说过,在方法上也可以使用@Config来配置,如果类级别与方法级别同时有@Config配置,方法级别上的配置会覆盖类级别的配置。
7.4.4 配置resource、assets、manifest路径
前面介绍配置constants属性时,Robolectric会自动加载build/intermediates目录下的资源文件,可以使用以下配置使Robolectric加载特定的资源文件。
@Config(assetDir = "some/build/path/assert",
resourceDir = "some/build/path/resourceDir",
manifest = "some/build/path/AndroidManifest.xml)
这里的路径很容易令人迷惑,必须要说明几点:
- 如果使用了@Config(constants = BuildConfig.class),资源文件的路径会固定为build目录。避免constants配置与自定义manifest配置一起使用,否则后者配置会不生效。
- manifest设置的目录base于Unit Test Config里面的”Working Directory”,具体如7.2里的图“设置JUnit的Working directory”所示。
- resourceDir、assetDir的目录base于manifest的父目录。
@Config(manifest = "src/test/AndroidManifest.xml", assetDir = "assetDir")
@Test
public void testConfigAssetDir() {
Application app = RuntimeEnvironment.application;
try {
InputStream inputStream = app.getAssets().open("test.txt");
int length = inputStream.available();
byte[] buffer = new byte[length];
inputStream.read(buffer);
inputStream.close();
String txt = new String(buffer);
System.out.println(txt);
inputStream.close();
} catch (IOException e){
e.printStackTrace();
}
}
自定义manifest目录示意图
该例子配置了一个指定的AndroidManifest.xml文件以及assets文件目录,测试程序读取assets里test.txt文件内容并打印出来。manifest的相对路径就是“UnitTest”工程里的“app”模块所在的文件路径,assetDir的相对路径就是AndroidManifest.xml文件的父目录路径。
7.4 Activity测试
7.4.1 生命周期
修改下7.3里面的测试案例代码如下,在各个生命周期回调中修改Button的文本内容。
public class MainActivity extends Activity {
Button mBtn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBtn = (Button) findViewById(R.id.btn_main);
mBtn.setText("onCreate");
mBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, RobolectricSampleActivity.class);
startActivity(intent);
}
});
}
@Override
protected void onStart() {
super.onStart();
mBtn.setText("onStart");
}
@Override
protected void onResume() {
super.onResume();
mBtn.setText("onResume");
}
@Override
protected void onPause() {
super.onPause();
mBtn.setText("onPause");
}
@Override
protected void onStop() {
super.onStop();
mBtn.setText("onStop");
}
@Override
protected void onDestroy() {
super.onDestroy();
mBtn.setText("onDestroy");
}
}
测试代码如下:
@Test
public void testActivityLifeCycle() {
ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class);
//会调用Activity的onCreate()方法
controller.create();
Button btn = (Button) controller.get().findViewById(R.id.btn_main);
System.out.println(btn.getText().toString());
controller.start();
System.out.println(btn.getText().toString());
controller.resume();
System.out.println(btn.getText().toString());
controller.pause();
System.out.println(btn.getText().toString());
controller.stop();
System.out.println(btn.getText().toString());
controller.destroy();
System.out.println(btn.getText().toString());
}
控制台打印结果如下所示:
onCreate
onStart
onResume
onPause
onStop
onDestroy
7.4.2 setupActivity()与buildActivity()
前面的示例中看到有2种创建Activity的方式:
//直接创建一个Activity,创建后的Activity会经历onCreate()->onStart()-onResume()这几个生命周期
MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);
//创建一个ActivityController,然后需要自己手动控制Activity的生命周期
ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class);
这2种方式有什么差别呢,查看setupActivity()的源码可以看到:
//实际上是调用了buildActivity()来创建Activity
public static <T extends Activity> T setupActivity(Class<T> activityClass) {
return buildActivity(activityClass).setup().get();
}
//这里手动控制了Activity的生命周期create()->start()->resume()
public ActivityController<T> setup() {
return create().start().postCreate(null).resume().visible();
}
7.4.3 测试Activity的跳转
在前面7.3中是示例中已经展示了。
7.4.4 测试Toast
//点击button弹出toast信息
mBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "toast sample", Toast.LENGTH_SHORT).show();
}
});
@Test
public void testToast() {
MainActivity activity = Robolectric.setupActivity(MainActivity.class);
Button btn = (Button) activity.findViewById(R.id.btn_main);
btn.performClick();
Assert.assertNotNull(ShadowToast.getLatestToast());
Assert.assertEquals("toast sample", ShadowToast.getTextOfLatestToast());
}
7.4.5 测试Dialog
@Test
public void testDialog() {
MainActivity activity = Robolectric.setupActivity(MainActivity.class);
Button btn = (Button) activity.findViewById(R.id.btn_main);
btn.performClick();
Assert.assertNotNull(ShadowAlertDialog.getLatestAlertDialog());
}
7.4.6 测试资源文件
@Test
public void testApplication() {
Application app = RuntimeEnvironment.application;
Context shadow = ShadowApplication.getInstance().getApplicationContext();
Assert.assertSame(shadow, app);
System.out.println(shadow.getResources().getString(R.string.app_name));
}
7.4.7 测试Fragment
@Test
public void testFragment() {
TestFragment fragment = new TestFragment();
//该方法会添加Fragment到Activity中
SupportFragmentTestUtil.startFragment(fragment);
Assert.assertThat(fragment.getView(), CoreMatchers.notNullValue());
}
7.5 Shadow Classes
顾名思义就是影子类,Robolectric定义了很多shadow class,用来修改或者扩展Android OS中类的行为。当一个Android中的类被实例化时,Robolectric会去寻找对应的影子类,如果找到了则会创建一个影子对象并与之相关联。每当Android类中的一个方法被调用时,Robolectric会保证其影子类中相应的方法会被先调用。这对所有的方法都适用,包括static和final类型的方法。
Shadows.shadowOf(...);
通过该方法几乎可以获取大部分Android类的shadow class,例如:
@Test
public void testShadow() {
MainActivity activity = Robolectric.setupActivity(MainActivity.class);
Button btn = (Button) activity.findViewById(R.id.btn_main);
ShadowActivity shadowActivity = Shadows.shadowOf(activity);
ShadowTextView shadowTextView = Shadows.shadowOf(btn);
}
Shadow Classes
public class Company {
public void welcome() {
System.out.println("method called in Company.");
}
public void sayHello() {
System.out.println("say hello in Company.");
}
}
//通过@Implements注解来声明shadow类
@Implements(Company.class)
public class ShadowCompany {
//通过@Implementation注解来标记shadow方法,此方法声明必须与原Company中的方法声明一致
@Implementation
public void welcome() {
System.out.println("method called in ShadowCompany.");
}
}
Shadows.shadowOf()方法不能作用于自定义shadow class,为了使Robolectric能够识别自定义shadow类,需要采用@Config注解,如下所示:
//通过shadows配置自定义的shadow class
@Config(shadows = {ShadowCompany.class})
@Test
public void testShadow() {
Company company = new Company();
company.welcome();
company.sayHello();
}
执行该测试方法,控制台打印结果如下:
method called in ShadowCompany.
say hello in Company.
从以上控制台打印结果中可以看到,welcome()方法实际执行的是ShadowCompany中的方法。
Shadowing Constructors
如果需要对构造函数进行shadow,必须实现__constructor__方法,并且该方法的参数必须与构造函数的参数一样。我们稍微修改前面的Company类以及ShadowCompany类,对其构造函数进行shadow。
public class Company {
private String name;
//构造函数有一个参数name
public Company(String name) {
this.name = name;
System.out.println("company constructor");
}
public void welcome() {
System.out.println("method called in Company.");
}
public void sayHello() {
System.out.println("say hello in Company.");
}
}
@Implements(Company.class)
public class ShadowCompany {
//必须实现该方法,参数与构造函数参数一样
public void __constructor__(String name) {
System.out.println("constructor in shadow class.");
}
@Implementation
public void welcome() {
System.out.println("method called in ShadowCompany.");
}
}
这个时候再执行测试代码,返回结果如下,可以看到Company的构造函数并没有执行。
constructor in shadow class.
method called in ShadowCompany.
say hello in Company.
Getting access to the real instance
有时shadow类需要使用它们关联的真实对象,可以通过@RealObject注解声明一个属性来实现。
@Implements(Company.class)
public class ShadowCompany {
//Robolectric会自动设置真实的关联对象
@RealObject
private Company company;
@Implementation
public void welcome() {
System.out.println("method called in ShadowCompany." + company.getName());
}
}
自定义shadow要点
- @Implements注解指定需要对哪个类进行shadow;
- @Implementation指定需要对哪个方法进行替换;
- 使用__constructor__来对构造器进行替换;
- @RealObject来引用真实的关联对象;
7.6 Robolectric的参数化测试
前面介绍JUnit4的时候讲到,JUnit4中有个叫Parameterized的test runner,能够实现参数化测试,同样Robolectric也提供了同样的功能。
@RunWith(ParameterizedRobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 23)
public class ParameterizedTest {
@ParameterizedRobolectricTestRunner.Parameters
public static List data() {
return Arrays.asList(new Integer[][] {
{1, 1},
{2, 2},
{3, 3},
{4, 4}
});
}
private int i;
private int j;
public ParameterizedTest(int i, int j) {
this.i = i;
this.j = j;
}
@Test
public void testParameter() {
System.out.println("parameter is " + i + ", " + j);
}
}
运行结果如下:
parameter is 1, 1
parameter is 2, 2
parameter is 3, 3
parameter is 4, 4
7.7 Robolectric的局限性
- 不支持JNI调用。凡是涉及到JNI调用的方法,都不能使用Robolectric来进行单元测试。对于复杂的应用,或多或少都会有JNI调用,可行的方案是设置一个全局变量来控制是否加载so库。
系列文章:
Android单元测试(一):前言
Android单元测试(二):什么是单元测试
Android单元测试(三):测试难点及方案选择
Android单元测试(四):JUnit介绍
Android单元测试(五):JUnit进阶
Android单元测试(六):Mockito学习
Android单元测试(七):Robolectric介绍
Android单元测试(八):怎样测试异步代码
网友评论