提到 android 自动化测试的时候经常会提到Instrumentation,但实际上 Instrumentation 是什么呢,很多人可能认为 Instrumentation 就是 android 的测试框架,实际上当启动一个 app 的时候都会实例化一个 Instrumentation 对象,且 Instrumentation 在每个 Activity 跳转的时候都会用到且其内部类 ActivityMonitor 会监控 activity 的,,只是我们不直接使用它;另外 Activity 的生命周期方法也是通过它来调用的:
在自动化测试过程中我们不是直接使用 Instrumentation 而且使用其子类 InstrumentationTestRunner,在测试工程的 AndroidManiFest.xml 里面配置如下:
<instrumentation
android:name="android.test.InstrumentationTestRunner"
android:label="InstrumentationApp"
android:targetPackage="com.instrumentation.app" >
</instrumentation>
上面两种方式在应用启动的时候初始化的 Instrumentation 对象是不同的;点击 app 图标启动 app 初始化的是默认值 Instrumentation 的对象,通过adb shell instrument方式启动 app 的时候初始化的是 AndroidManiFest.xml 里面配置的 InstrumentationTestRunner 对象,以上两种初始化方式都可以在阅读 android 源码的时候看到,这里提供一种直观简单的方式来验证我们的猜想。
查看 Activity.java 源码可知在 Activity 中存在 Instrumentation 的成员变量:
private Instrumentation mInstrumentation;
所以我们大致步骤就是通过反射获取 mInstrumentation 成员变量。
首先新建一个 Android project, 包名这里是:com.instrumentation.app,创建一个名为 MainActivity 的 Activity,
package com.instrumentation.app;
import java.lang.reflect.Field;
import android.app.Activity;
import android.util.Log;
public class MainActivity extends Activity {
private String LOG_STR = "debug";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Field mInstrumentation;
Object value = null;
try {
mInstrumentation = this.getClass().getSuperclass().getDeclaredField("mInstrumentation");
mInstrumentation.setAccessible(true);
value = mInstrumentation.get(this);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
}
Log.d(LOG_STR, "Instrumentation: "+value.getClass().getName());
}
}
打包工程安装到设备,点击 app 图标打开此时查看 ddms 的 log 显示:
此时显示的是默认的 Instrumentation 对象,当我们在此工程的 AndroidManiFest.xml 中配置如下:
<instrumentation
android:name="android.test.InstrumentationTestRunner"
android:label="InstrumentationApp"
android:targetPackage="com.instrumentation.app" >
</instrumentation>
此工程中新建一个测试类:AppDemoTest
import android.test.ActivityInstrumentationTestCase2;
public class AppDemoTest extends ActivityInstrumentationTestCase2<MainActivity>{
public AppDemoTest() {
super(MainActivity.class);
}
public void testApp(){
getActivity();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
打包工程安装到设备,通过命令行启动 case:
adb shell am instrument -e class com.instrumentation.app/AppDemoTest com.instrumentation.app/android.test.InstrumentationTestRunner
查看 ddms 的 log 显示:
另外对比 InstrumentationTestRunner 可以发现其主要实现了 Instrumentation 的 onCreate() 和 onStart() 方法,用于解析命令行传入的参数和执行 case。所以当我们想实际测试报告的输出也只要继续扩展 InstrumentationTestRunner 就可以了。
通过 robotium 获取当前的 Activity 对象
在看 robotium 框架源码之前需要分析 Instrumentation 启动 activity 的过程:
public void addMonitor(ActivityMonitor monitor) {
synchronized (mSync) {
if (mActivityMonitors == null) {
mActivityMonitors = new ArrayList();
}
mActivityMonitors.add(monitor);
}
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
IApplicationThread whoThread = (IApplicationThread) contextThread;
if (mActivityMonitors != null) {
synchronized (mSync) {
final int N = mActivityMonitors.size();
for (int i=0; i<N; i++) {
final ActivityMonitor am = mActivityMonitors.get(i);
if (am.match(who, null, intent)) {
am.mHits++;
if (am.isBlocking()) {
return requestCode >= 0 ? am.getResult() : null;
}
break;
}
}
}
}
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess();
int result = ActivityManagerNative.getDefault()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
}
return null;
}
public void callActivityOnResume(Activity activity) {
activity.mResumed = true;
activity.onResume();
if (mActivityMonitors != null) {
synchronized (mSync) {
final int N = mActivityMonitors.size();
for (int i=0; i<N; i++) {
final ActivityMonitor am = mActivityMonitors.get(i);
am.match(activity, activity, activity.getIntent());
}
}
}
}
以上通过 addMonitor() 方法将指定的 ActivityMonitor 对象添加到 list 中,在启动一个 activity 的过程中(代码仅供参考,实际流程复杂的多)会通过 for 循环对列表所有的 ActivityMonitor 对象调用 match 方法对其成员变量进行赋值操作,因此当此时调用 ActivityMonitor 的 getLastActivity 方法就可以获取刚启动的 activity 对象;
再看 Robotium 的 solo.getCurrentActivity() 最终实际调用在 robotium 框架中的 ActivityUtils 的 getCurrentActivity(true, true);
/**
* Returns the current {@code Activity}.
*
* @param shouldSleepFirst whether to sleep a default pause first
* @param waitForActivity whether to wait for the activity
* @return the current {@code Activity}
*/
public Activity getCurrentActivity(boolean shouldSleepFirst, boolean waitForActivity) {
if(shouldSleepFirst){
sleeper.sleep();
}
if(!config.trackActivities){
return activity;
}
if(waitForActivity){
waitForActivityIfNotAvailable();
}
if(!activityStack.isEmpty()){
activity=activityStack.peek().get();
}
return activity;
}
/**
* Waits for an activity to be started if one is not provided
* by the constructor.
*/
private final void waitForActivityIfNotAvailable(){
if(activityStack.isEmpty() || activityStack.peek().get() == null){
if (activityMonitor != null) {
Activity activity = activityMonitor.getLastActivity();
while (activity == null){
sleeper.sleepMini();
activity = activityMonitor.getLastActivity();
}
addActivityToStack(activity);
}
else if(config.trackActivities){
sleeper.sleepMini();
setupActivityMonitor();
waitForActivityIfNotAvailable();
}
}
}
/**
* This is were the activityMonitor is set up. The monitor will keep check
* for the currently active activity.
*/
private void setupActivityMonitor() {
if(config.trackActivities){
try {
IntentFilter filter = null;
activityMonitor = inst.addMonitor(filter, null, false);
} catch (Exception e) {
e.printStackTrace();
}
}
}
大概流程(不画图了 )
Instrumentation 是无法跨应用的,因此跨应用方案会单独采用 uiautomator 框架来做,但是 Google 在 API>=18 中通过 Instrumentation 提供获取 UiAutomation 对象来进行跨应用操作:
public UiAutomation getUiAutomation() {
if (mUiAutomationConnection != null) {
if (mUiAutomation == null) {
mUiAutomation = new UiAutomation(getTargetContext().getMainLooper(),
mUiAutomationConnection);
mUiAutomation.connect();
}
return mUiAutomation;
}
return null;
}
示例:Instrumentation 中调用 uiautomation 对象进行跨应用操作,回到桌面点击 “设置” 进入设置界面。
public void testApp(){
getActivity();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
//回到桌面
uiAutomation.performGlobalAction(AccessibilityService.GLOBAL_ACTION_HOME);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取当前界面的顶层AccessibilityNode信息
AccessibilityNodeInfo accessibilityNodeInfo = uiAutomation.getRootInActiveWindow();
List<AccessibilityNodeInfo> list = accessibilityNodeInfo.findAccessibilityNodeInfosByText("设置");
int size = list.size();
Log.d("debug", "size: "+size);
if(size != 0){
//获取“设置”控件信息
AccessibilityNodeInfo settingAccessibilityNodeInfo = list.get(0);
//执行点击操作
settingAccessibilityNodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
主要是利用 AIDL 提供远程调用,具体源码参考:https://github.com/hao-shen/AndToolsTest