JUnit之扩展IntrumentationTest框架

在上一篇文章“ JUnit之TestCase和TestSuite详解”中着重介绍了TestSuite和TestCase,本篇主要介绍JUnit在Android中的使用,以JUnit3执行引擎为例,介绍Intrumentation测试框架的使用。
在介绍Intrumenttation测试框架之前,需要先粘一下上一篇中的TestCase和TestSuite的结构图(图1),并附上InstrumentationTestCase要介绍的结构图(图2):

(图1)
(图2)
如图2所示,Android中的InstrumentationTestCase其实是TestCase的一个子类,如果对TestCase和TestSuite概念比较熟悉的话,Android中的InstrumentationTestCase框架便可以很好的上手了,InstrumentationTestCase的源码位于/source/frameworks/base/core/java/android/test中。
InstrumentationTestCase和ActivityTestCase以及AcitvityInstrumenttationTestCase的类图如图3所示,InstrumentationTestCase的核心是使用了Andoid的Instrumentation,Instrumentation是ActivityThread中调用Activity的一个中间层,我们可以暂且认为Instrumentation在测试过程中起到的作用是Android系统的hook,正如ActivityThread在调用Acitvity的一些生命周期方法的时候会通过Instrumentation对象调用相应的Activity方法一样,我们可以在我们的InstrumentationTestCase中通过其封装的mInstrumentation属性去模拟调用Activity的相关方法,如Activity的生命周期方法:onCreate、onStart、onResume、onPause、onStop、onDestroy,除此以外,还可以通过Instrumentation模拟启动相应的Activiry,并通过Activity的findViewById获取到相应的View控件,甚至可以通过Instrumentation去模拟向系统发送点击或者按键事件:

(图3)
正如前面所讲述的内容,AndroidJunit和Java的JUnit的扩展在于AndroidJunit集成了Instrumentation,因此 Instrumentation是整个InstrumentationTest框架的核心,在使用Eclipse进行单元测试的时候需要在清单文件中进行声明,声明格式如下:
 <instrumentation
            android:name = "android.test.InstrumentationTestRunner"
            android:targetPackage = "com.android.example"
            android:label = "Test"
            />


这样当开始执行测试的时候Android系统会根据配置选择所要使用的测试程序执行引擎,并通过测试执行引擎InstrumentationTestRunner执行测试用例,应用在启动的时候把将要使用的InstrumentationTestRunner传递到ActivityThread中,并调用ActivityThread中的handleBindApplication方法对Instrumentation进行初始化,注意在Android Studio中并不需要进行额外的instrumentation标签声明。
如图4所示InstrumentationTest框架的基类InstrumentationTestCase中封装了Instrumentation这个属性,并通过这个属性来启动具体的Activity(调用launchActivity方法),控制相关的代码在UI线程中执行,以及模拟按键向测试应用发送相应的按键等:

(图4)
通过getInstrumentation方法我们可以获取到这个Instrumentation属性,这个属性是在InstrumentTestSuite中被初始化的,如图5,在InstrumentationTestSuite的构造方法中会得到具体的Instrumentation对象,并在 InstrumentationTestSuite对象创建时将该参数作为该类的属性mInstrumentation的值,在执行重写自父类的runTest方法的时候会将这个值传入到InstrumentationTestCase中:

(图5)
//runTest方法
  @Override
    public void runTest(Test test, TestResult result) {

        if (test instanceof InstrumentationTestCase) {
            ((InstrumentationTestCase) test).injectInstrumentation(mInstrumentation);
        }

        // run the test as usual
        super.runTest(test, result);
    }
获取到Instrumentation以后,我们便可以模仿系统执行界面的相关操作,包括Activity的生命周期控制、按键事件等,具体的Instrumentation的方法可以参照Android提供的Instrumentation API文档,文档目录:/android-sdk/docs/reference/android/app/Instrumentation.html,如图6是常见的Activity的生命周期的控制方法,在这些方法中会通过回调调用Activity的相应的生命周期方法,如果注意过ActivityThread源码,ActivityThread正式通过执行Instrumentation的相关方法,来进一步执行Activity的相关生命周期方法,因此Instrumentation更像是一个接口,将代码进行分层,这样我们在做单元测试的时候需要执行到Activity的相关周期方法时,我们可以直接调用Instrumentation的相关方法,达到让Activity其相应方法的目的:

(图6)
InstrumentationTestCase中的另外一个比较核心的方法便是launchActivity方法,该方法的源码如下:
public final <T extends Activity> T launchActivity(
        String pkg,
        Class<T> activityCls,
        Bundle extras) {
    Intent intent = new Intent(Intent.ACTION_MAIN);
    if (extras != null) {
        intent.putExtras(extras);
    }
    return launchActivityWithIntent(pkg, activityCls, intent);
}
public final <T extends Activity> T launchActivityWithIntent(
        String pkg,
        Class<T> activityCls,
        Intent intent) {
    intent.setClassName(pkg, activityCls.getName());
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    T activity = (T) getInstrumentation().startActivitySync(intent);
    getInstrumentation().waitForIdleSync();
    return activity;
}
该方法的主要作用如名字那样,就是模拟启动一个Activity,如上面的代码,要启动这个Activity,借助的还是Instrumentation,就像我们在开发过程中要启动一个Activity需要传递Intent一样,在测试启动Activity的时候我们也应当传入一个Intent,然而这个Intent调用了它的addFrags方法,熟悉AMS中的Activity的Stack概念的应该比较清楚,当我们使用adb命令查看activity的时候会有stack概念和task的概念,系统的Launcher(桌面)activity会在一个单独的stack中,其他应用会在另外一个stack中,如果没有指明Activity的启动类型(如singleInstance),则每个应用都会有一个task,此应用的activity都会在这个task中,因此根据代码我们可以认为在执行单元测试的时候应用的Activity应该会在一个新的task中。
除此之外,如果待测试的Activity在执行的时候需要Intent传入数据进来才能达到测试目的的话,我们需要在这个Intent中加入携带的数据,在后面介绍ActivityInstrumentationTest2中的方法时我们会进一步强调Intent的作用。
runTestOnUiThread方法牵扯到一个比较核心的概念,那就是我们单元测试程序所在的线程,并非Android应用程序的主线程(UI线程),而是一个单独的线程,根据线程和进程的概念,为了能够访问到UI线程中的数据,测试线程又必须和UI线程在同一个进程中,因此,所有必须在Android的UI线程中执行的任务,需要放在runTestOnUiThread中才能够正常的执行,当然,除了可以调用此方法外,还可以在具体待测试的方法上加UiThreadTest注解,这样也可以保证整个方法能够在UI线程中执行。
sendKeys和sendRepeatedKeys则是在模仿用户的按键输入,比如按下返回键,按下菜单键等操作,一般在测试的时候都应该将触摸事件禁止掉,比如可以直接调用Instrumentation中的setTouchMode,并传入false,以便应用能够接受到键盘消息。这是因为如果触控模式打开,Android系统中有些控件是不能通过代码的方式设置输入焦点的,手指戳到一个控件后该控件就自然而然的获取到输入焦点了。例如,戳一个Button控件,除了导致其获取到焦点以外,还会触发它的点击事件。

(图7)
ActivityTestCase的主要方法如上图所示,ActivityTestCase中主要是以组合的形式对activity进行了简单的封装,并没有做太多的改动。

(图8)
ActivityInstrumentationTestCase2中构造方法中会传入一个指定的Activity的class,如果这个Activity不需要特殊的初始化数据,是可以正常的进行测试工作的,我们可以在需要使用Activity的地方直接调用getActivity方法获取这个Activity对象,并通过这个Activity对象去进行相应的操作,如获取控件,传递参数测试某一个方法等等。在前面介绍launchActivity的时候我们说过如果待测试的Activity需要传递相应的信息的才能正常工作的时候,我们必须将数据放入Intent中,并调用setActivityIntent去初始化要测试的Activity,并且注意,只有先调用了setActivityIntent再去调用getActivity方法才能正常的获取到待测试的Activity。看ActivityInstrumentationTestCase2重写的getActivity源码,在这个getActivity中会启动相应的Activity,并将Activity对象返回。代码如下:
@Override
public T getActivity() {
    Activity a = super.getActivity();
    if (a == null) {
        // set initial touch mode
        getInstrumentation().setInTouchMode(mInitialTouchMode);
        final String targetPackage = getInstrumentation().getTargetContext().getPackageName();
        // inject custom intent, if provided
        if (mActivityIntent == null) {
            a = launchActivity(targetPackage, mActivityClass, null);
        } else {
            a = launchActivityWithIntent(targetPackage, mActivityClass, mActivityIntent);
        }
        setActivity(a);
    }
    return (T) a;
}



介绍了这么多,最后通过一个简单的Demo来介绍InstrumentationTestCase框架的使用,在这个Demo中,我们模仿测试一个应用的启动页面,这个页面很简单,只有一个图片,要求是点击返回按键并不能销毁页面,过3s后自动跳转到MainActivity,并销毁此页面,布局如下:
<?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"
    android:background="#FFFFFF"
    android:orientation="vertical">
    <ImageView
        android:id="@+id/iv_launcher"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerCrop"
        android:src="@mipmap/ic_launcher" />
</LinearLayout>


Activity代码如下:
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.WindowManager;

import com.android.testdemo.R;

public class EntryActivity extends Activity {
    private static final int LAUNCHING_DURATION = 3000; // Stay here for 3s.
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_entry);
        new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
            @Override
            public void run() {
                Intent intent = new Intent(EntryActivity.this, MainActivity.class);
                startActivity(intent);
                finish();
            }
        }, LAUNCHING_DURATION);
    }
    @Override
    public void onBackPressed() {
//        启动页不允许返回
//        super.onBackPressed();
    }
}


Activity测试代码如下:

import android.app.Activity;
import android.os.Process;
import android.test.ActivityInstrumentationTestCase2;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.widget.ImageView;
import com.android.testdemo.R;
import junit.framework.TestCase;
import java.util.Locale;
public class EntryActivityTest extends ActivityInstrumentationTestCase2<EntryActivity> {
    private Activity mActivity;
    public static final String TAG = "EntryActivityTest";
    public EntryActivityTest() {
        super(EntryActivity.class);
    }

    public void setUp() throws Exception {
        super.setUp();
        setActivityInitialTouchMode(false);
        mActivity = getActivity();
    }

    public void tearDown() throws Exception {
        Log.e(TAG,"tearDown");
    }

    public void testOnResume(){
        final ImageView ivLauncher = (ImageView) mActivity.findViewById(R.id.iv_launcher);
        Log.e(TAG,String.format(Locale.getDefault(),"Test ThreadId = %s , Process ID = %s",Thread.currentThread().getId(), Process.myPid()));
        mActivity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                ivLauncher.performClick();
                Log.e(TAG,String.format(Locale.getDefault(),"UI ThreadId = %s ,Process ID = %s",Thread.currentThread().getId(), Process.myPid()));
            }
        });
        try{
            if(ivLauncher.getVisibility()!= View.VISIBLE){
                fail("ivLauncher is not visible");
            }
            if(mActivity.isFinishing()){
                fail("UnExcept click events on ImageView iv_launcher");
            }
        }catch (Exception e){

        }
    }

    public void testOnBackPressed() throws Exception {
        sendKeys(KeyEvent.KEYCODE_BACK);
        try {
            Thread.sleep(500);
            if(mActivity.isFinishing()){
                fail("onBackPressed is validate");
            }
        }catch (Exception e){
        }
    }
}



经过运行测试用例,可以测试通过,并且测试的Log日志如图9:

(图9)
通过对比日志,两个测试用例所在的线程确实不在UI线程当中,但是测试线程和UI线程在同一个进程当中。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

PentsunWang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值