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進行了哪些暗箱操作?
- 打包并安裝com.lcd.androidtestpractice包,這個包是我們的主程序App包,沒什么好說的。
- 打包并安裝com.lcd.androidtestpractice.test。原來,IDE幫我們打了一個新的apk包,包名為主包名后面加上
.test
后綴。我們找到對應的目錄,發現確實是存在對應的新apk。我們可以反編譯一下這個apk,看看這個包里都包含了哪些內容。下面給出反編譯后結果的幾張截圖:
AndroidTestCompile依賴的代碼
我們的測試代碼
代碼部分包含了build.gradle文件中androidTestCompile
指定要編譯的部分、包含有我們的測試代碼;再來看看AndroidManifest
文件的內容。
manifest文件
這個清單文件內容較少,首先里面沒有聲明任何的組件,所以安裝之后,不會在Launcher上看到對應的應用圖標。再仔細看看,我們發現了<instrumentation>標簽,里面指定了name
屬性值為android.support.test.runner.AndroidJUnitRunner
且targetPackage
屬性值為com.lcd.androidtestpractice
。targetPackage
屬性比較好理解,這個包名的值就是我們主程序的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)
來實現。這里有個例子可以看看 - 不多說了,我們現在再回過頭來看看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();
}
}
再來看看父類MonitoringInstrumentation
的onCreate
實現。
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官方的一張圖,請自行腦補!