背景
Mock、PowerMock、Junit等都只是在java层面的单元测试。但对于android app开发来说,单元测试需要运行在模拟器上或者真机上,不仅麻烦而且缓慢,而且一些依赖Android SDK的对象(如Activity,Button等)的测试非常头疼。那么Robolectric可以解决这些问题。
简介
我们可以使用Android提供的Instrumentation系统如ActivityUnitTestCase、ActivityInstrumentationTestCase2,将单元测试代码运行在模拟器或者是真机上。Google开源的测试框架如UIAutomator和Espresso也是基于Instrumentation的,更偏向于UI方面的自测化测试,要是应用在单元测试上速度也是很慢的。Robolectric通过实现一套JVM能运行的Android代码,然后在unit test运行的时候去截取android相关的代码调用,然后转到自己实现的代码去执行这个调用的过程。
Robolectric入门
依赖配置
在build.gradle 中配置Robolectric的依赖包
testCompile "org.robolectric:robolectric:3.2.1"
testCompile "org.robolectric:robolectric-annotations:3.2.1"
//robolectric针对support-v4的shadows
testCompile "org.robolectric:shadows-support-v4:3.2.1"
AndroidStudio 配置
1、在Build Variants面板中,将Test Artifact切换成Unit Tests模式(注:新版本的as已经不需要做这项配置),如下图:
2.Working directory设置
如果在运行测试方法过程中遇见如下异常:
java.io.FileNotFoundException: build\intermediates\bundles\debug\AndroidManifest.xml
......
或者:
No such manifest file: build/intermediates/bundles/debug/AndroidManifest.xml
......
解决的方式就是将Working directory的值设置为$MODULE_DIR$。
Activity的测试
我们写了一个Activity
public class DemoActivity extends BaseActivity implements View.OnClickListener{
private TextView mTextView;
private Button mForwardBtn;
private Button mDialogBtn;
private Button mAlertDialogBtn;
private Button mToastBtn;
private StudentDialog mAddDailog;
private Button mInverseBtn;
private CheckBox mCheckbox;
private Button mDelay;
public boolean isTaskFinish = false;
@Override
protected void loadViewLayout() {
setContentView(R.layout.test_demo_activity);
}
@Override
protected void loadIntent() {
}
@Override
protected void bindViews() {
mTextView = (TextView) findViewById(R.id.tv_lifecycle_label);
mForwardBtn = (Button) findViewById(R.id.forward);
mDialogBtn = (Button) findViewById(R.id.dialog);
mAlertDialogBtn = (Button) findViewById(R.id.alertdialog);
mToastBtn = (Button) findViewById(R.id.toast);
mInverseBtn = (Button) findViewById(R.id.btn_inverse);
mDelay = (Button) findViewById(R.id.delay);
mCheckbox = (CheckBox) findViewById(R.id.checkbox);
}
@Override
protected void processLogic(Bundle savedInstanceState) {
}
@Override
protected void setListener() {
mForwardBtn.setOnClickListener(this);
mDialogBtn.setOnClickListener(this);
mToastBtn.setOnClickListener(this);
mInverseBtn.setOnClickListener(this);
mDelay.setOnClickListener(this);
mAlertDialogBtn.setOnClickListener(this);
}
@Override
public void onClick(View v) {
if(v.getId() == R.id.forward){
forward();
}else if(v.getId() == R.id.dialog){
showDialog();
}else if(v.getId() == R.id.toast){
showToast("toast");
}else if(v.getId() == R.id.btn_inverse){
inverse();
}else if(v.getId() == R.id.delay) {
executeDelayedTask();
}else if(v.getId() == R.id.alertdialog) {
showAlertDialog();
}
}
@Override
protected void onStart() {
super.onStart();
mTextView.setText("onStart");
}
@Override
protected void onResume() {
super.onResume();
mTextView.setText("onResume");
}
@Override
protected void onPause() {
super.onPause();
mTextView.setText("onPause");
}
@Override
protected void onStop() {
super.onStop();
mTextView.setText("onStop");
}
@Override
protected void onDestroy() {
super.onDestroy();
mTextView.setText("onDestroy");
}
private void forward(){
intent2Activity(PageActivity.class);
}
private void showDialog(){
mAddDailog = new StudentDialog(this).setOnDialogClickListener(onDialogClickListener);
mAddDailog.show();
}
private void showToast(String message){
ToastUtils.showShort(message);
}
private void inverse(){
mCheckbox.setChecked(!mCheckbox.isChecked());
}
private void executeDelayedTask(){
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
isTaskFinish = true;
}
},2000);
}
public void showAlertDialog(){
AlertDialog alertDialog = new AlertDialog.Builder(this).setMessage("dialog")
.setTitle("dialog").create();
alertDialog.show();
}
private OnDialogClickListener onDialogClickListener = new OnDialogClickListener() {
@Override
public void onDialogClick(Dialog dialog, @ClickPosition String clickPosition) {
if(clickPosition.equals(ClickPosition.CANCEL)){
mAddDailog.cancel();
}else if(clickPosition.equals(ClickPosition.SUBMIT)){
showToast("添加成功");
}
}
};
}
DemoActivityTest类的配置
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class , sdk = 25)
public class DemoActivityTest {
private DemoActivity demoActivity;
private Button mForwardBtn;
private Button mDialogBtn;
private Button mToastBtn;
private Button mInverseBtn;
private Button mDelay;
private Button mAlertDialogBtn;
private CheckBox mCheckbox;
@Before
public void setUp() throws Exception {
demoActivity = Robolectric.setupActivity(DemoActivity.class);
mForwardBtn = (Button) demoActivity.findViewById(R.id.forward);
mDialogBtn = (Button) demoActivity.findViewById(R.id.dialog);
mToastBtn = (Button) demoActivity.findViewById(R.id.toast);
mInverseBtn = (Button) demoActivity.findViewById(R.id.btn_inverse);
mAlertDialogBtn = (Button) demoActivity.findViewById(R.id.alertdialog);
mDelay = (Button) demoActivity.findViewById(R.id.delay);
mCheckbox = (CheckBox) demoActivity.findViewById(R.id.checkbox);
}
}
在setUp()中将view初始化,和启动DemoActivity。
Config注解:
- 配置SDK版本: Robolectric会使用你在manifest中指定的targetSdkVersion版本来运行测试代码。如果你想测试在其它指定版本的表现,可以使用 sdk = 25。
- 配置Application类: Robolectric会根据manifest的配置自动帮你创建一个Application类,如果你希望提供一个自己实现的类,@Config(application = CustomApplication.class)
- 配置Resource路径:Robolectric为Gradle和Maven提供了默认的设置,但是也允许你修改这些资源的路径,包括manifest, resource目录,assets目录。如果你有一个自定义的生成脚本这会非常有用:@Config(manifest = "some/build/path/AndroidManifest.xml")
测试Activity.
@Test
public void testActivity() throws Exception {
//判断demoActivity不为空,启动成功
assertNotNull(demoActivity);
assertThat("true",demoActivity,is(notNullValue()));
}
测试Activity的生命周期
@Test
public void testActivityLifecycle() throws Exception{
ActivityController activityController = Robolectric.buildActivity(DemoActivity.class).create();
Activity activity = activityController.get();
TextView mTextView = (TextView) activity.findViewById(R.id.tv_lifecycle_label);
//调用start()方法,则DemoActivity中mTextView值设置成onStart
activityController.start();
//判断mTextView值是否是onStart
assertEquals("onStart",mTextView.getText().toString());
//onResume
activityController.resume();
assertEquals("onResume",mTextView.getText().toString());
//onPause
activityController.pause();
assertEquals("onPause",mTextView.getText().toString());
//onStop
activityController.stop();
assertEquals("onStop",mTextView.getText().toString());
//onStop
activityController.destroy();
assertEquals("onDestroy",mTextView.getText().toString());
}
测试Activity跳转
@Test
public void testStartActivity() throws Exception {
//点击,则跳转
mForwardBtn.performClick();
//目标Intent
Intent expectedIntent = new Intent(demoActivity, PageActivity.class);
//Robolectric启动的Intent
Intent actualIntent = ShadowApplication.getInstance().getNextStartedActivity();
assertEquals(expectedIntent.getComponent(),actualIntent.getComponent());
}
Dialog 测试.
@Test
public void testAlertDialog() throws Exception {
mAlertDialogBtn.performClick();
AlertDialog latestAlertDialog = ShadowAlertDialog.getLatestAlertDialog();
assertNotNull(latestAlertDialog);
}
Toast 测试
@Test
public void testToast() throws Exception {
mToastBtn.performClick();
Toast toast = ShadowToast.getLatestToast();
assertNotNull(toast);
assertEquals("toast",ShadowToast.getTextOfLatestToast());
}
测试View状态
@Test
public void testViewState() throws Exception {
assertTrue(mInverseBtn.isEnabled());
mCheckbox.setChecked(true);
//点击按钮,CheckBox反选
mInverseBtn.performClick();
assertTrue(!mCheckbox.isChecked());
mInverseBtn.performClick();
assertTrue(mCheckbox.isChecked());
}
测试资源文件访问
@Test
public void testResource() throws Exception {
Application application = RuntimeEnvironment.application;
String app_name = application.getString(R.string.app_name);
assertEquals("test-component",app_name);
}
测试延迟.
@Test
public void testDelay() throws Exception {
mDelay.performClick();
assertFalse(demoActivity.isTaskFinish);
//延迟执行完到UI主线程
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
assertTrue(demoActivity.isTaskFinish);
}
广播的测试
广播的测试点可以包含两个方面:
- 一是应用程序是否注册了该广播
- 二是广播接受者的处理逻辑是否正确
首先定义广播接收:
public class StaticReceiver extends BroadcastReceiver {
private String msg;
@Override
public void onReceive(Context context, Intent intent) {
//获取广播的数据.
msg = intent.getStringExtra("test");
LogHelper.i("静态广播接收消息....." + msg);
}
public String getMsg(){
return msg;
}
}
@Test
public void testBroadcase() throws Exception {
ShadowApplication application = ShadowApplication.getInstance();
Intent intent = new Intent();
intent.setAction("com.broadcast.static");
intent.putExtra("test","test data 123");
//测试是否注册广播接收者
assertTrue(application.hasReceiverForIntent(intent));
//广播接受者的处理逻辑是否正确
StaticReceiver receiver = new StaticReceiver();
receiver.onReceive(RuntimeEnvironment.application,intent);
assertEquals("test data 123",receiver.getMsg());
}
Service 测试
public class SimpleService extends Service {
private String msg;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
LogHelper.i("-------onCreate-------");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
msg = intent.getStringExtra("test");
LogHelper.i("-------test-------" + msg);
LogHelper.i("-------flags-------" + flags);
LogHelper.i("-------startId-------" + startId);
LogHelper.i("-------onStartCommand-------");
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
LogHelper.i("-------onDestroy-------");
super.onDestroy();
}
public String getMsg(){
return msg;
}
}
@Test
public void testService() throws Exception {
Application application = RuntimeEnvironment.application;
Intent intent = new Intent(application,SimpleService.class);
intent.putExtra("test","数据传输");
SimpleService serviceController = Robolectric.setupService(SimpleService.class);
serviceController.onStartCommand(intent,0,1);
assertEquals("数据传输",serviceController.getMsg());
}
默认Shadow测试
@Test
public void testDefaultShadow() throws Exception{
DemoActivity demoActivity = Robolectric.setupActivity(DemoActivity.class);
//通过Shadows.shadowOf()可以获取很多Android对象的Shadow对象
ShadowActivity shadowActivity = Shadows.shadowOf(demoActivity);
ShadowApplication application = Shadows.shadowOf(RuntimeEnvironment.application);
Bitmap bitmap = BitmapFactory.decodeFile("Path");
ShadowBitmap shadowBitmap = Shadows.shadowOf(bitmap);
//Shadow对象提供方便我们用于模拟业务场景进行测试的api
assertNull(shadowActivity.getNextStartedActivity());
assertNull(application.getNextStartedActivity());
assertNotNull(shadowBitmap);
}
自定义Shadow对象
我们有一个Person对象
public class Person {
private int id;
private String name;
public Person(int id,String name){
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
其次,创建Person的Shadow对象
@Implements(Person.class)
public class ShadowPerson {
@Implementation
public String getName() {
return "shadowPerson";
}
}
在测试用例中,ShadowPerson对象将自动代替原始对象,调用Shadow对象的数据和行为。要记得在DemoActivityTest类上面@Config中加shadows = {ShadowPerson.class}
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class , sdk = 25,shadows = {ShadowPerson.class})
public class DemoActivityTest {
}
@Test
public void testCustomShadow() throws Exception {
Person person = new Person(1,"genius");
//getName()实际上调用的是ShadowPerson的方法
assertEquals("shadowPerson", person.getName());
//获取Person对象对应的Shadow对象
ShadowPerson shadowPerson = (ShadowPerson) ShadowExtractor.extract(person);
assertEquals("shadowPerson", shadowPerson.getName());
}
测试自定义Dialog
比如我们有一个StudentDialog继承Dialog,要怎么测试这个自定义Dialog。
首先自定义一个Shadow继承于ShadowDialog,然后重写show()方法。
@Implements(StudentDialog.class)
public class ShadowStudentDialog extends ShadowDialog {
@Implementation
public void show() {
super.show();
shadowOf(RuntimeEnvironment.application).setLatestDialog(this);
}
}
@Test
public void testDialog() throws Exception {
mDialogBtn.performClick();
Dialog latestDialog = ShadowDialog.getLatestDialog();
assertNotNull(latestDialog);
}