美文网首页
用Instrumentation改良monkey工具实战

用Instrumentation改良monkey工具实战

作者: 闲富北路的拉法 | 来源:发表于2017-05-29 17:35 被阅读0次

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

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

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

    SpeedFight

    1. 问题及分析

    1.1 现象

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

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


    Monkey WebviewMonkey 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 cmdmonkey cmd

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

    generateEventsgenerateEvents

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

    generateMotionEventgenerateMotionEvent

    1.3 结论分析

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

    有以下几点:

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

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

    testgoodstestgoods

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

    相关文章

      网友评论

          本文标题:用Instrumentation改良monkey工具实战

          本文链接:https://www.haomeiwen.com/subject/axiqfxtx.html