Android 单元测试之Robolectric

背景

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已经不需要做这项配置),如下图:

Android 单元测试之Robolectric_第1张图片
rob_as.png

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$。

Android 单元测试之Robolectric_第2张图片
edit conf.jpg
Android 单元测试之Robolectric_第3张图片
working dir.jpg

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注解:

  1. 配置SDK版本: Robolectric会使用你在manifest中指定的targetSdkVersion版本来运行测试代码。如果你想测试在其它指定版本的表现,可以使用 sdk = 25。
  2. 配置Application类: Robolectric会根据manifest的配置自动帮你创建一个Application类,如果你希望提供一个自己实现的类,@Config(application = CustomApplication.class)
  3. 配置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);
}

广播的测试

广播的测试点可以包含两个方面:

  1. 一是应用程序是否注册了该广播
  2. 二是广播接受者的处理逻辑是否正确

首先定义广播接收:

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);
}

你可能感兴趣的:(Android 单元测试之Robolectric)