用Instrumentation改良monkey工具實戰(zhàn)

這里Monkey不是猴子牺陶,而是Android系統(tǒng)中用來做自動化測試的工具伟阔,即盲點(diǎn)、壓力測試掰伸。

在之前的移動端產(chǎn)品迭代中皱炉,Monkey工具一直沒有利用起來。開發(fā)同學(xué)忙于需求狮鸭,測試同學(xué)資源較少合搅,自動化測試工具欠缺多搀,重視不夠。版本發(fā)布的流程灾部,壓力測試這一環(huán)節(jié)是完全缺失的康铭。crash沒有在發(fā)版前提前發(fā)現(xiàn),也造成我們線上產(chǎn)品crash率較高赌髓。

App不同于H5从藤,一旦發(fā)布版本,其更新成本锁蠕、周期是比較高的夷野。所以應(yīng)當(dāng)將發(fā)版前的質(zhì)量保證作為第一要務(wù),確比偾悖可靠性悯搔。

SpeedFight

1. 問題及分析

1.1 現(xiàn)象

monkey工具的用法,網(wǎng)上有很多資料舌仍,在此不作介紹妒貌。可參考:UI/Application Exerciser Monkey

用法很簡單抡笼。但是苏揣,我們在初步使用monkey的過程中,幾乎必然進(jìn)入一個較深的路徑中推姻,再也無法跳出來——可能是在兩個頁面、或者Dialog框沟、Input面板間不斷的切換藏古,始終沒法關(guān)閉頁面,逐級跳出忍燥。在我測試的過程中拧晕,發(fā)現(xiàn)幾乎都是進(jìn)入了一個webview頁面:


Monkey Webview
Monkey Webview

monkey走入了死胡同,一直在一個小圈子里梅垄、幾個頁面間打轉(zhuǎn)厂捞,無法發(fā)揮作用。

1.2 探索

monkey的實現(xiàn)原理队丝,參考源碼:monkey

當(dāng)敲下

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

實際是通過執(zhí)行一段shell腳本靡馁,啟動monkey.jar。入口在Monkey.java:main()方法當(dāng)中机久。

monkey cmd
monkey cmd

通過調(diào)整--pct-touch, --pct-motion, --pct-syskeys, --pct-appswitch等參數(shù)比例臭墨,monkey會隨機(jī)生成相應(yīng)事件(MonkeySourceRandom.java::generateEvents()):

generateEvents
generateEvents

monkey產(chǎn)生touch事件的坐標(biāo)位置是完全隨機(jī)的(MonkeySourceRandom.java::generateMotionEvent()):

generateMotionEvent
generateMotionEvent

1.3 結(jié)論分析

所以,到這里膘盖,基本上可以對上面的問題做一個解答胧弛,即:為什么monkey會進(jìn)入幾個頁面后無法跳出尤误?

有以下幾點(diǎn):

  • touch事件點(diǎn)擊的位置是全屏幕隨機(jī)的;
  • webview中頁面幾乎是每個地方都可以點(diǎn)擊,并且點(diǎn)擊后跳到另一個頁面;
  • 雖然頁面左上角有返回鍵结缚、也有物理Back鍵损晤,但是返回鍵所占的區(qū)域只是屏幕上很小一部分,大約只占屏幕點(diǎn)擊事件總數(shù)的1/80(按面積計算)红竭, 物理Back鍵也只占所有SYS_KEYS中的1/7尤勋。這里多么類似于生物蟻群算法,進(jìn)入死循環(huán)就仿佛是找到了最短路徑德崭。但遺憾的是斥黑,monkey的目的是希望能夠最大程度覆蓋所有可能的執(zhí)行路徑。繼續(xù)進(jìn)入下一個頁面的可能性永遠(yuǎn)比退出去更多眉厨,除非這個頁面的有效點(diǎn)擊區(qū)域變小才能增大退出來的可能性锌奴。

有贊微商城App中一個典型的webview頁面:

testgoods
testgoods

2. 解決方案

如果監(jiān)聽每個activity的啟動過程,并且判斷它的存活時間憾股,當(dāng)認(rèn)為已經(jīng)太長了鹿蜀,主動將其finish掉。這似乎是個可行的方案服球。由此想到用Instrumentation, 通過Instrumentaion啟動App茴恰,再開啟monkey測試,不就能控制頁面深度及存活時間斩熊。

這里需要特別注意的是:關(guān)閉activity的策略往枣,該如何定制?如果策略不合理粉渠,很可能造成

    1. 比較深的頁面跑不到;
    1. 單頁面的點(diǎn)擊分冈,測試完整度不夠

目前我所使用的策略是:

    1. topActivity,沒有切換的情況下霸株,最長存活時間為15s
    1. 當(dāng)前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集成進(jìn)App項目代碼中,并在AndroidManifest.xml中聲明
 <instrumentation
      android:name="com.youzan.testtool.MonkeyInstrumentation"
      android:targetPackage="${MONKEY_TEST_PACKAGE}" >
  </instrumentation>

其中 MONKEY_TEST_PACKAGE 為待測包名尤溜,另注意修改MonkeyInstrumentaion所在包名倔叼。
編譯安裝好Apk

  • 啟動instrumentation, 目標(biāo)進(jìn)程啟動并監(jiān)聽activity棧存活狀態(tài)
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
  • 結(jié)果查看

4. 綜述

monkey這個工具,看起來很簡單靴跛,但使用起來還是會遇到這樣的坑缀雳。以前有專職的測試同學(xué)替我們完成monkey,測試梢睛,導(dǎo)致對遇到的問題也沒有去深究肥印。

發(fā)版前的自動化測試识椰,包括UT、UI測試深碱、monkey腹鹉、內(nèi)存、性能及流暢度敷硅、Apk Size等等功咒,越來越成為上線發(fā)版流程中不可或缺的一環(huán),我們在不斷的建設(shè)完善當(dāng)中绞蹦。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末力奋,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子幽七,更是在濱河造成了極大的恐慌景殷,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件澡屡,死亡現(xiàn)場離奇詭異猿挚,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)驶鹉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進(jìn)店門绩蜻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人室埋,你說我怎么就攤上這事办绝。” “怎么了姚淆?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵八秃,是天一觀的道長。 經(jīng)常有香客問我肉盹,道長,這世上最難降的妖魔是什么疹尾? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任上忍,我火速辦了婚禮,結(jié)果婚禮上纳本,老公的妹妹穿的比我還像新娘窍蓝。我一直安慰自己,他們只是感情好繁成,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布吓笙。 她就那樣靜靜地躺著关摇,像睡著了一般芳杏。 火紅的嫁衣襯著肌膚如雪疲扎。 梳的紋絲不亂的頭發(fā)上冲甘,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天,我揣著相機(jī)與錄音叁鉴,去河邊找鬼土涝。 笑死,一個胖子當(dāng)著我的面吹牛幌墓,可吹牛的內(nèi)容都是我干的但壮。 我是一名探鬼主播,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼常侣,長吁一口氣:“原來是場噩夢啊……” “哼蜡饵!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起胳施,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤溯祸,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后巾乳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體您没,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年胆绊,在試婚紗的時候發(fā)現(xiàn)自己被綠了氨鹏。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,561評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡压状,死狀恐怖仆抵,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情种冬,我是刑警寧澤镣丑,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站娱两,受9級特大地震影響莺匠,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜十兢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一趣竣、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧旱物,春花似錦遥缕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春户秤,著一層夾襖步出監(jiān)牢的瞬間码秉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工虎忌, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留泡徙,地道東北人。 一個月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓膜蠢,卻偏偏與公主長得像堪藐,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子挑围,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評論 2 359

推薦閱讀更多精彩內(nèi)容