Instrumentation Test Framework

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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容