Instrumentation Test Framework

作者: chandarlee | 来源:发表于2016-06-29 20:02 被阅读2009次

    Instrumentation Test Class VS JUnit Test Class

    @RunWith(AndroidJUnit4.class)
    public class MainActivityTest {
    
        @Rule
        public ActivityTestRule<MainActivity> activity = new ActivityTestRule<MainActivity>(MainActivity.class);
    
        @Test
        public void testTxt(){
            Espresso.onView(withId(R.id.textview0)).check(matches(withText("Hello World")));
        }
    
    }
    

    上面的代码给出的是一个很简单的Instrumentation Test Class。在代码中我们看到了熟悉的@Rule@Test注解。其实,除此之外,我们还能够使用JUnit支持的其他大部分注解,包括@Suite@ClassRule@BeforeClass@AfterClass@Before@After等等,只是在这个Test Class中没有体现出来而已。可以这样说,一个Instrumentation Test Class本质上就是一个JUnit Test Class。因为Android Instrumentation Test本来就是基于JUnit框架的。如果非要说它们之间的区别,那就只能是它们的默认Runner不同。对于JUnit4而言, 它的默认Runner是BlockJUnit4ClassRunner(请参照前面一篇文章),而对于Android Test而言,一个Test Class的默认Runner就是上面的代码中@RunWith所指明的AndroidJUnit4类。具体类的定义如下:

    public final class AndroidJUnit4 extends AndroidJUnit4ClassRunner { ...}
    
    public class AndroidJUnit4ClassRunner extends BlockJUnit4ClassRunner { ...}
    

    AndroidJUnit4只是AndroidJUnit4ClassRunner的一个别名而已(让你调皮,取那么长的类名),而AndroidJUnit4ClassRunner其实又是继承于BlockJUnit4ClassRunner的。查看其源码,可以发现AndroidJUnit4ClassRunner的执行逻辑99%都交由BlockJUnit4ClassRunner处理,也就是上一篇文章所分析的流程,而它们唯一的一点区别就是AndroidJUnit4ClassRunner@Test中的timeout的处理稍有不同,这里不再具体分析。所以我们可以这样说,一个Instrumentation Test Class的执行流程同一个Normal JUnit Test Class是一致的。这里所说的执行流程指的仅仅是Test Class对应的Runner执行的逻辑,不包括Runner的构造和Instrumentation Test的入口流程,这个流程我们放在下文进行分析。

    About Instrumentation

    我们先来看看Instrumentation Test中的Instrumentation到底是什么,它又是干嘛的?不卖关子了,Instrumentation其实是Android Framework中的一个类,它的作用简而言之就是能够监控Android系统和我们Application之间的交互。我们都知道,一个Application有一个ActivityThread对象,负责和ActivityMangerService打交道来管理App的运行,比如启动某个Activity,发送广播等其他操作。而这个Instrumentation会在App启动阶段被初始化,然后作为一个实例变量保存到ActivityThread对象中。Application的创建、Activity生命周期方法的回调等其他操作,都会经过Instrumentation来完成。e.g.

    public Application makeApplication(boolean forceDefaultAppClass,
                Instrumentation instrumentation) {
            ...
                app = mActivityThread.mInstrumentation.newApplication(
                        cl, appClass, appContext);
            ...
        }
    
    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
                ...
                activity = mInstrumentation.newActivity(
                        cl, component.getClassName(), r.intent);
                ...
                mInstrumentation.callActivityOnCreate(activity, r.state);
                ...
                mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state,
                        r.persistentState);
                ...
                mInstrumentation.callActivityOnPostCreate(activity, r.state);
                        
                ...
            }
    

    这里不再一一列举,可以自行查看Instrumentation的代码。那这个Instrumentation在Android Test中有什么作用呢?在App正常运行的时候,系统会帮助App维护运行组件的状态和信息,这些管理是有必要的,因为这个过程是非常复杂的,交由开发者自己去完成很容易造成系统的混乱。系统管理的好处就是简单方便,但同时也造成了开发者不能很方便的得到运行组件的信息,好在正常运行的极大多数情况下我们都不需要访问这些信息。然而当我们在测试时,就另当别论了,我们可能需要频繁地访问当前正在运行的某个组件的信息,比如Activity。这个时候,Instrumentation就派上用场了。有些时候我们可能需要扩展当前的Instrumentation类,为此Android允许我们在AndroidManifest文件中创建<instrumentaion>标签,用来指定在创建Instrumentation时使用我们自定义的类,<instrumentaion>中需要至少包含以下两个属性:

    • android:name:指定使用这个类来创建,而不是系统默认的Instrumentation类,需要为Instrumentation的子类才合法。
    • android:targetPackage:指定要监控的目标app包名;这里一般是指我们需要测试的目标app包名。

    请注意,Android仅在开发者进行app测试的时候,也就是说只能在我们的Instrumentation Test Class的测试代码中才能访问到Instrumentation。当App正常运行时没有可供访问的开放接口使用(当然不排除某些黑科技方法),这一点非常重要。当然,这并不代表app正常运行时,系统没有构建Instrumentation对象;只是系统在这个时候会忽略我们的<instrumentaion>标签,创建一个默认的Instrumentation对象。

    Run An Instrumentation Test Class

    下面,我们通过Android Studio来运行上面的Test Class,来看看IDE是怎么做的?当然,我们首先需要在build.gradle文件中添加下面的配置:

    android {
        ...
        defaultConfig {
            ...
            testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
        }
        ...
    }
    

    现在,我们先不用去考虑上面的配置到底起了的是什么作用?右键Test Class->Run 起来再说,同时请注意观察Run窗口的相应输出。直接粘贴如下:

    $ adb push D:\AndroidCode\StudioCode\AndroidTestPractice\app\build\outputs\apk\app-debug.apk /data/local/tmp/com.lcd.androidtestpractice
    $ adb shell pm install -r "/data/local/tmp/com.lcd.androidtestpractice"
    pkg: /data/local/tmp/com.lcd.androidtestpractice
    Success

    $ adb push D:\AndroidCode\StudioCode\AndroidTestPractice\app\build\outputs\apk\app-debug-androidTest-unaligned.apk /data/local/tmp/com.lcd.androidtestpractice.test
    $ adb shell pm install -r "/data/local/tmp/com.lcd.androidtestpractice.test"
    pkg: /data/local/tmp/com.lcd.androidtestpractice.test
    Success

    Running tests

    $ adb shell am instrument -w -r -e debug false -e class com.lcd.androidtestpractice.MainActivityTest com.lcd.androidtestpractice.test/android.support.test.runner.AndroidJUnitRunner
    Client not ready yet..Test running started
    Tests ran to completion.

    Run窗口的输出可以很清晰的看到IDE进行了哪些暗箱操作?

    1. 打包并安装com.lcd.androidtestpractice包,这个包是我们的主程序App包,没什么好说的。
    2. 打包并安装com.lcd.androidtestpractice.test。原来,IDE帮我们打了一个新的apk包,包名为主包名后面加上.test后缀。我们找到对应的目录,发现确实是存在对应的新apk。我们可以反编译一下这个apk,看看这个包里都包含了哪些内容。下面给出反编译后结果的几张截图:
      AndroidTestCompile依赖的代码
      我们的测试代码
      代码部分包含了build.gradle文件中androidTestCompile指定要编译的部分、包含有我们的测试代码;再来看看AndroidManifest文件的内容。
      manifest文件
      这个清单文件内容较少,首先里面没有声明任何的组件,所以安装之后,不会在Launcher上看到对应的应用图标。再仔细看看,我们发现了<instrumentation>标签,里面指定了name属性值为android.support.test.runner.AndroidJUnitRunnertargetPackage属性值为com.lcd.androidtestpracticetargetPackage属性比较好理解,这个包名的值就是我们主程序的app包名,这里的意思就是指定它为要测试的目标app;再来看看name属性,我们发现这个值和我们刚刚在build.gradle中配置的testInstrumentationRunner属性值相等。它们会不会有什么联系呢?Bingo!当我们在build.gradle文件中运用testInstrumentationRunner 'customname'时,Android Studio在打包测试APK时就会在manifest中添加<Instrumentation>标签,并且将name属性值指定为customname。那这里为什么要指定testInstrumentationRunner值为AndroidJUnitRunner呢?能不能是另外一个其他的值呢?当然可以!这里的值其实只需要是指向一个Instrumentation类或者子类的全域限定的类名。我们之所以指定为AndroidJUnitRunner是因为Android将测试代码的执行逻辑放到这个类中,测试代码就靠它来运行的。显然,我们完全可以实现一个自定义类继承于AndroidJUnitRunner,并通过testInstrumentationRunner来声明使用它,这样不仅不会阻碍我们测试代码的运行,还可以通过覆写它的某些方法来达成某些目标。这在某些情况下很有用,比如我们想要自定义测试时创建的Application对象,就可以通过继承AndroidJUnitRunner并重写public Application newApplication(ClassLoader cl, String className, Context context)来实现。这里有个例子可以看看
    3. 不多说了,我们现在再回过头来看看IDE执行的第三步。

    adb shell am instrument -w -r -e debug false -e class com.lcd.androidtestpractice.MainActivityTest com.lcd.androidtestpractice.test/android.support.test.runner.AndroidJUnitRunner

    其实就是通过adb运行am instrument命令。所以AndroidTest也是可以通过ADB手动运行的,IDE只是为我们简化了流程。来看具体的命令,很明显里面指定的MainActivityTest就是我们要执行的Test Class,而com.lcd.androidtestpractice.test/android.support.test.runner.AndroidJUnitRunner这一部分其实标识了一个Instrumentation。前半部分com.lcd.androidtestpractice.test为apk包名,后半部分AndroidJUnitRunner为目标类名,两个部分加起来就唯一确定了一个Instrumentation对象。命令中其他各种配置参数和使用方法就不多说了,更多详情在这里。我们关注的是这个命令到底干了些什么?

    Am Instrument

    Am Instrument命令会调用Am中的runInstrument()方法,在这个方法中,解析输入的参数并最终将请求发送到ActivityManagerService中的startInstrumentation方法。好吧,一切还是得由AMS来完成。

    public boolean startInstrumentation(ComponentName className,
                String profileFile, int flags, Bundle arguments,
                IInstrumentationWatcher watcher, IUiAutomationConnection uiAutomationConnection,
                int userId, String abiOverride) { 
                ...
                InstrumentationInfo ii = null; //包含当前App的<Instrumentation>标签信息
                ApplicationInfo ai = null; //包含目标App的<Application>标签信息
                try {
                    ii = mContext.getPackageManager().getInstrumentationInfo(
                        className, STOCK_PM_FLAGS);
                    ai = AppGlobals.getPackageManager().getApplicationInfo(
                            ii.targetPackage, STOCK_PM_FLAGS, userId);
                } catch (PackageManager.NameNotFoundException e) {
                } catch (RemoteException e) {
                }
                
                //通过PackageManager检查目标App和测试App的签名是否相同
                //只有签名相同才能进行Instrumentation Test
                //签名不相同,失败并抛出异常
                int match = mContext.getPackageManager().checkSignatures(
                        ii.targetPackage, ii.packageName);
                if (match < 0 && match != PackageManager.SIGNATURE_FIRST_NOT_SIGNED) {
                    ...
                    reportStartInstrumentationFailure(watcher, className, msg);
                    throw new SecurityException(msg);
                }
    
                //先停止当前的Target App
                forceStopPackageLocked(ii.targetPackage, -1, true, false, true, true, false, userId,
                        "start instr");
    
                //重新启动目标App进程
                ProcessRecord app = addAppLocked(ai, false, abiOverride);
                //记录必要信息,注意这里是唯一赋值的地方
                //所以只有从这个入口过去的,才会创建Instrumentation实例
                //否则,即使在manifest中指定,也不会创建对应实例
                app.instrumentationClass = className;
                app.instrumentationInfo = ai;
                app.instrumentationArguments = arguments;
                ...
            }
    
            return true;
        }
    

    这里传入startInstrumentation的参数className是一个Component对象,它是在Am中由com.lcd.androidtestpractice.test/android.support.test.runner.AndroidJUnitRunner解析过来的,而argument这个bundle在这里则包含有我们需要执行的Test Class类名。其他流程请看上面代码中的注释说明。这里要强调一下,一旦检查通过,该方法会停止当前正在运行的目标App,然后重新启动目标进程。当目标进程的ActivityThread对象创建以后,会通过attachApplication()方法请求Ams给它绑定一个Application。来看看Ams的处理:

    private final boolean attachApplicationLocked(IApplicationThread thread, int pid) {
                ...
                //执行Test App和Target App的dexopt
                ensurePackageDexOpt(app.instrumentationInfo != null
                        ? app.instrumentationInfo.packageName
                        : app.info.packageName);
                if (app.instrumentationClass != null) {
                    ensurePackageDexOpt(app.instrumentationClass.getPackageName());
                }
                
                //因为app.instrumentationInfo已经在startInstrumentation方法中赋值为目标App的ApplicationInfo
                //所以不为空
                ApplicationInfo appInfo = app.instrumentationInfo != null
                        ? app.instrumentationInfo : app.info;
                ...
                //返回到ActivityThread中去处理,这里的appInfo为Target App的ApplicationInfo
                thread.bindApplication(processName, appInfo, providers, app.instrumentationClass,
                        profilerInfo, app.instrumentationArguments, app.instrumentationWatcher,
                        app.instrumentationUiAutomationConnection, testMode, enableOpenGlTrace,
                        isRestrictedBackupMode || !normalMode, app.persistent,
                        new Configuration(mConfiguration), app.compat,
                        getCommonServicesLocked(app.isolated),
                        mCoreSettingsObserver.getCoreSettingsLocked());
                ...
        }
    

    之后,辗转回到ActivityThread的handleBindApplication方法。

    private void handleBindApplication(AppBindData data) {
            mBoundApplication = data;
            
            //data.appInfo是Ams传过来的参数,为target app的ApplicationInfo
            //所以这里会根据ApplicationInfo去load Target Apk    
            data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
    
            //创建一个Target App的context对象
            final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);
            ...
            //同样是从Ams传过来的参数
            //= 'com.lcd.androidtestpractice.test/android.support.test.runner.AndroidJUnitRunner`
            if (data.instrumentationName != null) {
                InstrumentationInfo ii = null;
                try {
                //获取<instrumentation>标签的信息
                    ii = appContext.getPackageManager().
                        getInstrumentationInfo(data.instrumentationName, 0);
                } catch (PackageManager.NameNotFoundException e) {
                }
                ...
                //记录Instrumentation相关信息
                mInstrumentationPackageName = ii.packageName; 
                mInstrumentationAppDir = ii.sourceDir;//Test App的路径
                mInstrumentationSplitAppDirs = ii.splitSourceDirs;
                mInstrumentationLibDir = ii.nativeLibraryDir;
                mInstrumentedAppDir = data.info.getAppDir();//Target App的路径
                mInstrumentedSplitAppDirs = data.info.getSplitAppDirs();
                mInstrumentedLibDir = data.info.getLibDir();
    
                //根据InstrumentataionInfo构造对应Test App的ApplicationInfo
                ApplicationInfo instrApp = new ApplicationInfo();
                instrApp.packageName = ii.packageName;
                instrApp.sourceDir = ii.sourceDir;
                instrApp.publicSourceDir = ii.publicSourceDir;
                instrApp.splitSourceDirs = ii.splitSourceDirs;
                instrApp.splitPublicSourceDirs = ii.splitPublicSourceDirs;
                instrApp.dataDir = ii.dataDir;
                instrApp.nativeLibraryDir = ii.nativeLibraryDir;
    
                //根据ApplicationInfo Load Test App
                //appContext.getClassLoader()是target app的classloader
                //这里传入它作为test apk的base class loader
                LoadedApk pi = getPackageInfo(instrApp, data.compatInfo,
                        appContext.getClassLoader(), false, true, false);
    
                //构造一个Test App的context对象
                ContextImpl instrContext = ContextImpl.createAppContext(this, pi);
    
                try {
                    //这个classloader对应为test app的classloader,所以能够加载test apk中的类
                    java.lang.ClassLoader cl = instrContext.getClassLoader();
                    //从test apk中创建一个Instrumentation实例对象
                    //这里其实就是构造一个android.support.test.runner.AndroidJUnitRunner实例
                    mInstrumentation = (Instrumentation)
                        cl.loadClass(data.instrumentationName.getClassName()).newInstance();
                } catch (Exception e) {
                    ...
                }
    
                //初始化
                mInstrumentation.init(this, 
                        instrContext,/*对应test app的context,从Instrumentation调用getContext就返回这个context*/
                       appContext,/*对应target app的context,从Instrumentation调用getTargetContext就返回这个context*/
                       new ComponentName(ii.packageName, ii.name), data.instrumentationWatcher,
                       data.instrumentationUiAutomationConnection);
    
                ...
            } else {
                //创建一个系统默认的Instrumentation对象
                mInstrumentation = new Instrumentation();
            }
            //创建application对象,这里data.info指向target app的loadedapk对象
            Application app = data.info.makeApplication(data.restrictedBackupMode,null);
            mInitialApplication = app;
            ...
            //调用Instrumentation的onCreate方法
            mInstrumentation.onCreate(data.instrumentationArgs);
            ...
        }
    

    具体的说明请看上面的注释。上面的方法调用之后,ActivityThread中的mPackages变量会包含两个LoadedApk,分别对应Test app和target app。我们可以看到,通过使用Instrumentation,Android将Test App和Target App同时加载到了同一个进程中。到这里,我们已经创建了AndroidJUnitRunner对象实例。来看看onCreate方法。

    public class AndroidJUnitRunner extends MonitoringInstrumentation {
    
            ...
            @Override
            public void onCreate(Bundle arguments) {
                //保存并解析参数
                mArguments = arguments;
                parseRunnerArgs(mArguments);
    
                //调用父类实现
                super.onCreate(arguments);
    
                ...
                start();
            }
        }
    

    再来看看父类MonitoringInstrumentationonCreate实现。

    public void onCreate(Bundle arguments) {
            
            //向InstrumentationRegistry中注册这个Instrumentation实例
            //这样在测试代码中,就可以通过InstrumentationRegistry.getInstrumentation()方法获取这个实例
            InstrumentationRegistry.registerInstance(this, arguments);
            ActivityLifecycleMonitorRegistry.registerInstance(mLifecycleMonitor);
            ApplicationLifecycleMonitorRegistry.registerInstance(mApplicationMonitor);
            IntentMonitorRegistry.registerInstance(mIntentMonitor);
    
            mHandlerForMainLooper = new Handler(Looper.getMainLooper());
            final int corePoolSize = 0;
            final long keepAliveTime = 0L;
            mExecutorService = new ThreadPoolExecutor(corePoolSize, Integer.MAX_VALUE, keepAliveTime,
                    TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), new ThreadFactory() {
                @Override
                public Thread newThread(Runnable runnable) {
                    Thread thread = Executors.defaultThreadFactory().newThread(runnable);
                    thread.setName(MonitoringInstrumentation.class.getSimpleName());
                    return thread;
                }
            });
            Looper.myQueue().addIdleHandler(mIdleHandler);
            //调用Instrumentation的onCreate方法,空方法,不关注
            super.onCreate(arguments);
            specifyDexMakerCacheProperty();
            setupDexmakerClassloader();
        }
    

    onCreate方法结束之后,AndroidJUnitRunner紧接着调用start()方法。start()的实现位于基类Instrumentation中,如下:

    public void start() {
            if (mRunner != null) {
                throw new RuntimeException("Instrumentation already started");
            }
            mRunner = new InstrumentationThread("Instr: " + getClass().getName());
            mRunner.start();
        }
    
    private final class InstrumentationThread extends Thread {
            ...
            public void run() {
                ...
                onStart();
            }
        }
    

    InstrumentationThread是一个线程类,在start()方法中会新建这个线程并启动,线程转而执行onStart()方法。该方法的具体实现在AndroidJUnitRunner中。

    @Override
        public void onStart() {
            ...
            TestExecutor.Builder executorBuilder = new TestExecutor.Builder(this);
            addListeners(mRunnerArgs, executorBuilder);
            TestRequest testRequest = buildRequest(mRunnerArgs, getArguments());
            results = executorBuilder.build().execute(testRequest);
            ...
            finish(Activity.RESULT_OK, results);
        }
    

    山重水复疑无路,很接近了,耐心!在onStart()这个方法中构建了一个TestRequest,然后又构建一个TestExecutor,并调用其execute()方法执行这个test request,最后调用finish()方法。我们先来看看finish做了什么?

    public void finish(int resultCode, Bundle results) {
            ...
            mThread.finishInstrumentation(resultCode, results);
        }
    

    finish方法通过ActivityThread的finishInstrumentation方法通知Ams完成测试工作,Ams最后来做一些收尾的清理工作并结束当前进程。代码就不给出了。所以到finish的时候,我们的测试工作已经完成了,所以我们可以肯定我们的测试代码就是通过TestExecutor来运行的。现在回头来看看它的execute()方法,看看具体怎么执行。

    public Bundle execute(TestRequest testRequest) {
            ...
            JUnitCore testRunner = new JUnitCore();
            setUpListeners(testRunner);
            junitResults = testRunner.run(testRequest.getRequest());
            junitResults.getFailures().addAll(testRequest.getFailures());
            ...
        }
    

    柳暗花明又一村啊!execute()方法中构建了一个JUnitCore对象,调用其run(request)方法执行。等等,这里的JUnitCore不就是我们上篇分析的JUnit执行Test Class的入口吗?还需要继续吗?我想,到这里应该可以告一段落了,因为往后的执行逻辑跟我们前面分析的JUnit的执行逻辑是一样一样的。这里在提一点,Android通过覆写AllDefaultPossibilitiesBuilder来为Test Class生成默认Runner。

    class AndroidRunnerBuilder extends AllDefaultPossibilitiesBuilder {
    
        ...
        public AndroidRunnerBuilder(AndroidRunnerParams runnerParams) {
            super(true);
            mAndroidJUnit3Builder = new AndroidJUnit3Builder(runnerParams);
            mAndroidJUnit4Builder = new AndroidJUnit4Builder(runnerParams);
            mAndroidSuiteBuilder = new AndroidSuiteBuilder(runnerParams);
            mAndroidAnnotatedBuilder = new AndroidAnnotatedBuilder(this, runnerParams);
            mIgnoredBuilder = new IgnoredBuilder();
        }
    
        ...
    }
    

    Android在为一个Test Class构造Runner时,使用的就是这个RunnerBuilder。该RunnerBuilder提供了基于不同JUnit版本的默认Android Runner版本。比如我们的Test Class不显示的声明@RunWith,那么基于JUnit4,RunnerBuilder给我们构造的就是AndroidJUnit4ClassRunner,其实就是AndroidJUnit4这个Runner。

    最后的最后,再提一点。那就是class loader的问题。因为是两个apk运行在同一个进程里面,怎么保证类的加载不会出错呢?其实Android这点已经为我们处理了。在ActivityThread中有几个成员变量,保存了Test App和Target App的apk路径信息。

        String mInstrumentationPackageName = null; //test app package name
        String mInstrumentationAppDir = null; //test app apk path
        String[] mInstrumentationSplitAppDirs = null;
        String mInstrumentationLibDir = null;
        String mInstrumentedAppDir = null; //target app apk path
        String[] mInstrumentedSplitAppDirs = null;
        String mInstrumentedLibDir = null;
    

    这些信息用于LoadedApk构建相应的classloader,从而可以满足从test app或者target app中正确的load我们想要的类。具体可以看LoadedApk中的getClassLoader()方法,这里就不再讲述。最后的最后的最后,给出android官方的一张图,请自行脑补!

    Getting Started with Testing Android Developers.png

    相关文章

      网友评论

        本文标题:Instrumentation Test Framework

        本文链接:https://www.haomeiwen.com/subject/sjwtjttx.html