这里Monkey不是猴子,而是Android系统中用来做自动化测试的工具,即盲点、压力测试。
在之前的移动端产品迭代中,Monkey工具一直没有利用起来。开发同学忙于需求,测试同学资源较少,自动化测试工具欠缺,重视不够。版本发布的流程,压力测试这一环节是完全缺失的。crash没有在发版前提前发现,也造成我们线上产品crash率较高。
App不同于H5,一旦发布版本,其更新成本、周期是比较高的。所以应当将发版前的质量保证作为第一要务,确保可靠性。
1. 问题及分析
1.1 现象
monkey工具的用法,网上有很多资料,在此不作介绍。可参考:UI/Application Exerciser Monkey
用法很简单。但是,我们在初步使用monkey的过程中,几乎必然进入一个较深的路径中,再也无法跳出来——可能是在两个页面、或者Dialog、Input面板间不断的切换,始终没法关闭页面,逐级跳出。在我测试的过程中,发现几乎都是进入了一个webview页面:
monkey走入了死胡同,一直在一个小圈子里、几个页面间打转,无法发挥作用。
1.2 探索
monkey的实现原理,参考源码:monkey
当敲下
adb shell monkey -p PACKAGE_NAME --throttle XX --pct-touch XX --pct-motion XX --pct-syskeys XX --pct-appswitch XX -s XX -v -v COUNT > monkey_text.txt
实际是通过执行一段shell脚本,启动monkey.jar。入口在Monkey.java:main()方法当中。
通过调整--pct-touch, --pct-motion, --pct-syskeys, --pct-appswitch等参数比例,monkey会随机生成相应事件(MonkeySourceRandom.java::generateEvents()):
monkey产生touch事件的坐标位置是完全随机的(MonkeySourceRandom.java::generateMotionEvent()):
1.3 结论分析
所以,到这里,基本上可以对上面的问题做一个解答,即:为什么monkey会进入几个页面后无法跳出?
有以下几点:
- touch事件点击的位置是全屏幕随机的;
- webview中页面几乎是每个地方都可以点击,并且点击后跳到另一个页面;
- 虽然页面左上角有返回键、也有物理Back键,但是返回键所占的区域只是屏幕上很小一部分,大约只占屏幕点击事件总数的1/80(按面积计算), 物理Back键也只占所有SYS_KEYS中的1/7。这里多么类似于生物蚁群算法,进入死循环就仿佛是找到了最短路径。但遗憾的是,monkey的目的是希望能够最大程度覆盖所有可能的执行路径。继续进入下一个页面的可能性永远比退出去更多,除非这个页面的有效点击区域变小才能增大退出来的可能性。
有赞微商城App中一个典型的webview页面:
2. 解决方案
如果监听每个activity的启动过程,并且判断它的存活时间,当认为已经太长了,主动将其finish掉。这似乎是个可行的方案。由此想到用Instrumentation, 通过Instrumentaion启动App,再开启monkey测试,不就能控制页面深度及存活时间。
这里需要特别注意的是:关闭activity的策略,该如何定制?如果策略不合理,很可能造成
- 比较深的页面跑不到;
- 单页面的点击,测试完整度不够
目前我所使用的策略是:
- topActivity,没有切换的情况下,最长存活时间为15s
- 当前Activity栈中,从上往下,第一层存活时间30s,每层递增30s,超过时间后依次finish弹出
- 每个task最长存活时间10分钟
MonkeyInstrumentation源码附上:
public class MonkeyInstrumentation extends Instrumentation {
private static final String TAG = "MONKEY_INSTRUMENT";
// config params
private long checkTaskInterval = 5000; // 5s
private long topActivitySurvivalTime = 15*1000; // 15s
private long stackActivitySurvivalTimeFirstLevel = 30*1000; // 30s
private long stackActivitySurvivalTimeIncremental = 30*1000; // 30s
private long taskSurvivalTime = 10*60*1000; // 10min
private Handler handler = null;
private ActivityManager activityManager = null;
private List<Activity> activityList = null;
private SparseArray<Long> survivalTimeMap = null;
private Activity currentActivity = null;
private long currentActivitySurvivalTime = 0;
private SparseArray<Long> taskSurvivalTimeMap = null;
public MonkeyInstrumentation() {
super();
}
@Override
public void callApplicationOnCreate(Application app) {
super.callApplicationOnCreate(app);
handler = new Handler();
activityList = new ArrayList<>();
survivalTimeMap = new SparseArray<>();
taskSurvivalTimeMap = new SparseArray<>();
Log.e(TAG, "call application on create, app:" + app);
postCheckTask();
}
@Override
public void callActivityOnCreate(final Activity activity, Bundle icicle) {
super.callActivityOnCreate(activity, icicle);
int index = activityList.size();
activityList.add(activity);
long now = System.currentTimeMillis();
survivalTimeMap.put(index, now);
int taskId = activity.getTaskId();
Log.e(TAG, "create activity, activity:" + activity + ", taskId:" + taskId + ", index:" + index + ", now:" + now);
if (taskSurvivalTimeMap.get(taskId, 0L) == 0) {
taskSurvivalTimeMap.put(taskId, now);
}
}
@Override
public void callActivityOnResume(Activity activity) {
super.callActivityOnResume(activity);
currentActivity = activity;
currentActivitySurvivalTime = System.currentTimeMillis();
}
@Override
public void callActivityOnPause(Activity activity) {
super.callActivityOnPause(activity);
}
@Override
public void callActivityOnDestroy(final Activity activity) {
super.callActivityOnDestroy(activity);
int index = activityList.indexOf(activity);
if (index >= 0) {
activityList.remove(index);
survivalTimeMap.remove(index);
}
}
private void postCheckTask() {
handler.postDelayed(new Runnable() {
@Override
public void run() {
Log.e(TAG, "post check task run");
checkActivityStatus();
postCheckTask();
}
}, checkTaskInterval);
}
private void checkActivityStatus() {
Log.e(TAG, "to checkActivityStatus");
checkCurrentActivity();
checkStackActivity();
checkCurrentStack();
}
private void checkCurrentActivity() {
Log.e(TAG, "checkCurrentActivity");
if (currentActivity != null){
if (System.currentTimeMillis() - currentActivitySurvivalTime > topActivitySurvivalTime) { // 15s
Log.e(TAG, "checkCurrentActivity, to finish a long time activity:" + currentActivity);
currentActivity.finish();
currentActivity = null;
currentActivitySurvivalTime = 0;
}
}
}
private void checkCurrentStack() {
Log.e(TAG, "checkCurrentStack");
if (activityManager == null) {
Context context = getContext();
if (context != null) {
activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
}
}
if (activityManager != null) {
long now = System.currentTimeMillis();
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
List<ActivityManager.AppTask> appTaskList = activityManager.getAppTasks();
if (appTaskList != null && appTaskList.size() > 0) {
ActivityManager.AppTask appTask = appTaskList.get(0);
int taskId = appTask.getTaskInfo().id;
Long taskTime = taskSurvivalTimeMap.get(taskId);
if (taskTime != null && now - taskTime > taskSurvivalTime) {
Log.e(TAG, "finish and remove appTask:" + appTask);
for (int i = activityList.size() - 1; i >= 0; --i) {
if (activityList.get(i).getTaskId() == taskId) {
activityList.remove(i);
survivalTimeMap.remove(i);
}
}
appTask.finishAndRemoveTask();
}
}
} else {
List<ActivityManager.RunningTaskInfo> runningTaskInfoList = activityManager.getRunningTasks(1);
if (runningTaskInfoList != null && runningTaskInfoList.size() > 0) {
ActivityManager.RunningTaskInfo runningTaskInfo = runningTaskInfoList.get(0);
int taskId = runningTaskInfo.id;
Long taskTime = taskSurvivalTimeMap.get(taskId);
if (taskTime != null && now - taskTime > taskSurvivalTime) {
Log.e(TAG, "finish and remove runningTask:" + runningTaskInfo);
for (int i = activityList.size(); i >= 0; --i) {
Activity activity = activityList.get(i);
if (activity.getTaskId() == taskId) {
activityList.remove(i);
survivalTimeMap.remove(i);
activity.finish();
}
}
}
}
}
} else {
Log.e(TAG, "checkActivityStatus, activityManager is null");
}
}
private void checkStackActivity() {
Log.e(TAG, "checkStackActivity");
int len = activityList.size();
long time = stackActivitySurvivalTimeFirstLevel;
long now = System.currentTimeMillis();
Activity needClearActivity = null;
for (int i = len - 1; i > 0; --i) {
if (now - survivalTimeMap.get(i, 0L) > time) {
needClearActivity = activityList.get(i);
break;
}
time += stackActivitySurvivalTimeIncremental; // increment every level
}
if (needClearActivity != null) {
Log.e(TAG, "needClearActivity:" + needClearActivity);
// to clear activity above needClearActivty in this task
int id = needClearActivity.getTaskId();
for (int i = len - 1; i > 0; --i) {
Activity activity = activityList.get(i);
if (activity.getTaskId() == id) {
Log.e(TAG, "clearStackActivity, activity:" + activity);
activityList.remove(i);
survivalTimeMap.remove(i);
activity.finish();
}
}
}
}
}
3. 使用
- 将 MonkeyInstrumentation集成进App项目代码中,并在AndroidManifest.xml中声明
<instrumentation
android:name="com.youzan.testtool.MonkeyInstrumentation"
android:targetPackage="${MONKEY_TEST_PACKAGE}" >
</instrumentation>
其中 MONKEY_TEST_PACKAGE 为待测包名,另注意修改MonkeyInstrumentaion所在包名。
编译安装好Apk
- 启动instrumentation, 目标进程启动并监听activity栈存活状态
adb shell am instrument MONKEY_TEST_PACKAGE/RUNNER_CLASS
其中RUNNER_CLASS即为MonkeyInstrumentation
- 启动 monkey测试
adb shell monkey -p MONKEY_TEST_PACKAGE --throttle 300 --pct-touch 60 --pct-motion 15 --pct-syskeys 10 --pct-appswitch 15 -s `date +%H%M%S` -v -v -v --monitor-native-crashes --ignore-timeouts --hprof --bugreport COUNT > monkey_test.txt
- 结果查看
4. 综述
monkey这个工具,看起来很简单,但使用起来还是会遇到这样的坑。以前有专职的测试同学替我们完成monkey,测试,导致对遇到的问题也没有去深究。
发版前的自动化测试,包括UT、UI测试、monkey、内存、性能及流畅度、Apk Size等等,越来越成为上线发版流程中不可或缺的一环,我们在不断的建设完善当中。