用Instrumentation改良monkey工具实战

这里Monkey不是猴子,而是Android系统中用来做自动化测试的工具,即盲点、压力测试。

在之前的移动端产品迭代中,Monkey工具一直没有利用起来。开发同学忙于需求,测试同学资源较少,自动化测试工具欠缺,重视不够。版本发布的流程,压力测试这一环节是完全缺失的。crash没有在发版前提前发现,也造成我们线上产品crash率较高。

App不同于H5,一旦发布版本,其更新成本、周期是比较高的。所以应当将发版前的质量保证作为第一要务,确保可靠性。

用Instrumentation改良monkey工具实战_第1张图片
SpeedFight

1. 问题及分析

1.1 现象

monkey工具的用法,网上有很多资料,在此不作介绍。可参考:UI/Application Exerciser Monkey

用法很简单。但是,我们在初步使用monkey的过程中,几乎必然进入一个较深的路径中,再也无法跳出来——可能是在两个页面、或者Dialog、Input面板间不断的切换,始终没法关闭页面,逐级跳出。在我测试的过程中,发现几乎都是进入了一个webview页面:


Monkey 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()方法当中。

monkey cmd

通过调整--pct-touch, --pct-motion, --pct-syskeys, --pct-appswitch等参数比例,monkey会随机生成相应事件(MonkeySourceRandom.java::generateEvents()):

generateEvents

monkey产生touch事件的坐标位置是完全随机的(MonkeySourceRandom.java::generateMotionEvent()):

generateMotionEvent

1.3 结论分析

所以,到这里,基本上可以对上面的问题做一个解答,即:为什么monkey会进入几个页面后无法跳出?

有以下几点:

  • touch事件点击的位置是全屏幕随机的;
  • webview中页面几乎是每个地方都可以点击,并且点击后跳到另一个页面;
  • 虽然页面左上角有返回键、也有物理Back键,但是返回键所占的区域只是屏幕上很小一部分,大约只占屏幕点击事件总数的1/80(按面积计算), 物理Back键也只占所有SYS_KEYS中的1/7。这里多么类似于生物蚁群算法,进入死循环就仿佛是找到了最短路径。但遗憾的是,monkey的目的是希望能够最大程度覆盖所有可能的执行路径。继续进入下一个页面的可能性永远比退出去更多,除非这个页面的有效点击区域变小才能增大退出来的可能性。

有赞微商城App中一个典型的webview页面:

testgoods

2. 解决方案

如果监听每个activity的启动过程,并且判断它的存活时间,当认为已经太长了,主动将其finish掉。这似乎是个可行的方案。由此想到用Instrumentation, 通过Instrumentaion启动App,再开启monkey测试,不就能控制页面深度及存活时间。

这里需要特别注意的是:关闭activity的策略,该如何定制?如果策略不合理,很可能造成

    1. 比较深的页面跑不到;
    1. 单页面的点击,测试完整度不够

目前我所使用的策略是:

    1. topActivity,没有切换的情况下,最长存活时间为15s
    1. 当前Activity栈中,从上往下,第一层存活时间30s,每层递增30s,超过时间后依次finish弹出
    1. 每个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 activityList = null;
    private SparseArray survivalTimeMap = null;

    private Activity currentActivity = null;
    private long currentActivitySurvivalTime = 0;

    private SparseArray 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 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 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中声明
 
  

其中 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等等,越来越成为上线发版流程中不可或缺的一环,我们在不断的建设完善当中。

你可能感兴趣的:(用Instrumentation改良monkey工具实战)