Instrumentation Test Class
VS JUnit Test Class
@RunWith(AndroidJUnit4.class)
public class MainActivityTest {
@Rule
public ActivityTestRule activity = new ActivityTestRule(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文件中创建
- android:name:指定使用这个类来创建,而不是系统默认的Instrumentation类,需要为Instrumentation的子类才合法。
- android:targetPackage:指定要监控的目标app包名;这里一般是指我们需要测试的目标app包名。
请注意,Android仅在开发者进行app测试的时候,也就是说只能在我们的Instrumentation Test Class的测试代码中才能访问到Instrumentation。当App正常运行时没有可供访问的开放接口使用(当然不排除某些黑科技方法),这一点非常重要。当然,这并不代表app正常运行时,系统没有构建Instrumentation对象;只是系统在这个时候会忽略我们的
标签,创建一个默认的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,看看这个包里都包含了哪些内容。下面给出反编译后结果的几张截图:
代码部分包含了build.gradle文件中androidTestCompile
指定要编译的部分、包含有我们的测试代码;再来看看AndroidManifest
文件的内容。
这个清单文件内容较少,首先里面没有声明任何的组件,所以安装之后,不会在Launcher上看到对应的应用图标。再仔细看看,我们发现了标签,里面指定了 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中添加标签,并且将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的标签信息
ApplicationInfo ai = null; //包含目标App的标签信息
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 {
//获取标签的信息
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(), 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官方的一张图,请自行脑补!