用Instrumentation改良monkey工具实战

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

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

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

SpeedFight

1. 问题及分析

1.1 现象

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

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


Monkey 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
monkey cmd

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

generateEvents
generateEvents

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

generateMotionEvent
generateMotionEvent

1.3 结论分析

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

有以下几点:

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容