移动测试基础 Android Instrumentation 框架简单说明

土拨熊 · December 06, 2016 · Last by Hi Hydra replied at August 29, 2018 · 7113 hits
本帖已被设为精华帖!

提到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源码的时候看到,这里提供一种直观简单的方式来验证我们的猜想。

获取Instrumentation对象

查看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框架对Instrumentation的利用

通过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();
}
}
}

大概流程(不画图了😄

  • robotium中通过Instrumentation对象初始化一个ActivityMonitor对象,activityMonitor = inst.addMonitor(filter, null, false);
  • 一旦有activity启动则activityMonitor就会调用match方法给其成员变量赋值,此时调用activityMonitor.getLastActivity()方法获取最新的activity对象并保存到Stack中;
  • 调用ActivityUtils的getCurrentActivity(true, true),从取出栈顶的对象即为最新的activity对象;

扩展:关于Instrumentation对uiautomator的利用

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();
}
}

扩展:解决通过Instrumentation进行自动化测试依赖被测应用权限问题

主要是利用AIDL提供远程调用,具体源码参考:https://github.com/hao-shen/AndToolsTest

共收到 7 条回复 时间 点赞

写的很好。

思寒_seveniruby 将本帖设为了精华贴 06 Dec 17:18

加精理由: 提供了对android基本原理的解释. 对instrumentation的理解也很正确. 这是优秀工程师的基本功. 在源码分析上体现了作者优秀的探索精神.

感谢分享。受教了

感谢分享朴实无华的好文

新人路过, 代码复制过来为什么报错啊, 那个ActivityInstrumentationTestCase2导不进去, 不知道怎么回事, 求大神指导

需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up