這里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ù),確比偾悖可靠性悯搔。
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走入了死胡同,一直在一個小圈子里梅垄、幾個頁面間打轉(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)中机久。

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

monkey產(chǎn)生touch事件的坐標(biāo)位置是完全隨機(jī)的(MonkeySourceRandom.java::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頁面:

2. 解決方案
如果監(jiān)聽每個activity的啟動過程,并且判斷它的存活時間憾股,當(dāng)認(rèn)為已經(jīng)太長了鹿蜀,主動將其finish掉。這似乎是個可行的方案服球。由此想到用Instrumentation, 通過Instrumentaion啟動App茴恰,再開啟monkey測試,不就能控制頁面深度及存活時間斩熊。
這里需要特別注意的是:關(guān)閉activity的策略往枣,該如何定制?如果策略不合理粉渠,很可能造成
- 比較深的頁面跑不到;
- 單頁面的點(diǎn)擊分冈,測試完整度不夠
目前我所使用的策略是:
- topActivity,沒有切換的情況下霸株,最長存活時間為15s
- 當(dāng)前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集成進(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)中绞蹦。