需求
自动化测试过程中提高指定模块的测试率:每一个版本的迭代,需要对这个版本进行重点测试,这时就需要配置页面的覆盖比例,重点模块重点测试
构想
利用MonkeyRunner干预Monkey的事件流,在Monkey自动化测试的过程中,在特定的情境下触发MonkeyRunner指定跳转页面,Monkey继续模拟点击
环境
Monkey相关
安装Android SDK 并配置环境变量
http://pan.baidu.com/s/1jIlifbS
MonkeyRunner相关
jdk、android sdk、python编译器(如果是使用命令行运行python脚本的方式)
工具
Monkey
功能:模拟用户触摸屏幕、滑动Trackball、 按键等操作来对设备上的程序进行压力测试,简单的讲就是,瞎点
简介:
- Monkey程序由Android系统自带,使用Java诧言写成,在Android文件系统中的存放路径是: /system/framework/monkey.jar
- Monkey.jar程序是由一个名为“monkey”的Shell脚本来启动执行,shell脚本在Android文件系统中 的存放路径是:/system/bin/monkey
- 启动方式
(1) 可以通过PC机CMD窗口中执行: adb shell monkey {+命令参数}来进行Monkey测试
(2) 在PC上adb shell 进入Android系统,通过执行 monkey {+命令参数} 来进行Monkey 测试
(3) 在Android机或者模拟器上直接执行monkey 命令,可以在Android机上安装Android终端模拟器
MonkeyRunner
功能:它提供一个API,运用该API编写的程序可以不用通过android代码来直接控制Android设备和模拟器,我们可以写一个Python程序对android应用程序或测试包进行安装,运行,发送模拟击键
vs Monkey:Monkey产生的事件都是随机的,不可控的;Monkeyrunner既能按照用户的需求来产生特定序列的特定事件,还能截屏保存和比较图片,编写简单。借助于python,也能实现强大的功能。
DO IT
正文开始!
掌控MonkeyRunner
目的:
(1) 监听应用是否跳出,如果跳出则跳往指定的页面
(2) 指定时间间隔,每过一段时间跳转到指定页面
-
监听跳出
首先,执行cmd指令adb devices 可得到如下信息
其中3488170为当前与pc相连的手机设备号
执行指令:adb -s 3483170 shell dumpsys activity top 可得到如下信息
其中第二行的信息则是当前设备中栈顶activity的信息
于是,综合以上信息则可判断当前应用是否跳出
接下来,java代码的实现:
/**
* 判断某个设备口袋贵金属是否在前台
*
* @param deviceId
* @return
*/
public boolean watchActivity(String deviceId) {
boolean isAlive = false;
Runtime run = Runtime.getRuntime();
Process proc;
try {
// proc = run.exec("adb shell dumpsys activity top");
//监听指定的设备
proc = run.exec("adb -s " + deviceId + " shell dumpsys activity top");
BufferedReader in = new BufferedReader(new InputStreamReader(
proc.getInputStream()));
StringBuffer stringBuffer = new StringBuffer();
String line = null;
while ((line = in.readLine()) != null) {
if (line.contains(pakageName)) {
isAlive = true;
break;
}
}
in.close();
} catch (Exception e) {
e.printStackTrace();
}
return isAlive;
}
/**
* 对多设备监听跳出,并执行跳转
*/
public void checkForStartActivity() {
System.out.println("----checkForStartActivity------");
Runtime run = Runtime.getRuntime();
Process proc;
try {
proc = run.exec("adb devices");
BufferedReader in = new BufferedReader(new InputStreamReader(
proc.getInputStream()));
String line = null;
while ((line = in.readLine()) != null) {//遍历所有的设备id
if ((line.contains("device") || line.contains("offline")) && !line.contains("devices")) { //排除第一行和最后一行的数据
String deviceId = line.replace("device", "").trim();
if (line.contains("device")) {
deviceId = line.replace("device", "").trim();
}
if (line.contains("offline")) {
deviceId = line.replace("offline", "").trim();
}
System.out.println("DeviceId=" + deviceId + "----isAlive=" + watchActivity(deviceId));
if (!watchActivity(deviceId)) { // 对某一设备监听是否跳出
startActivityByJava(deviceId); // 跳出则跳往指定的页面
}
}
}
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* java 启动monkeyrunner
*
* @param deviceId 设备id
*/
public void startActivityByJava(String deviceId) {
if (adb == null) {
adb = new AdbBackend();
//这里需要注意一下adb的类型
}
// 参数分别为自己定义的等待连接时间和设备id
device = (AdbMonkeyDevice) adb.waitForConnection(1000, deviceId);
//添加启动权限
String action = "android.intent.action.MAIN";
Collection categories = new ArrayList();
categories.add("android.intent.category.LAUNCHER");
// 启动要测试的主界面
if (device != null) {
for (int i = 0; i < activityPaths.length; i++) {
device.startActivity(null, action, null, null, categories,
new HashMap(), activityPaths[i], 0);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
device.dispose(); // 需要dispose操作 否则无法再次启动activity
} else {
startActivityByPython(runnerPath, pythonPath); //如果java启动失败就python启动
}
}
上述代码为启动MonkeyRunner的核心代码,其中为java和python的双启动方式(java失败则启动python)
由于python的启动是辅助的启动方式,这里不作详细介绍,读者可自行百度或参考:
http://www.cnblogs.com/lynn-li/p/5885001.html
至于java的启动方式则略显复杂,详细可参考:
http://www.cnblogs.com/nuliniaoboke/archive/2012/11/23/2784385.html
关于高版本java对MonKeyRunner的调用:
http://blog.csdn.net/dyllove98/article/details/8797009
- 定时器循环强制跳转
目的:每过一段时间强制跳转到指定页面,以达到重点模块重点测试的效果
/**
* 强制跳转activity
*
* @param
*/
public void forceJumpActivity() {
System.out.println("----forceJumpActivity------");
Runtime run = Runtime.getRuntime();
Process proc;
try {
proc = run.exec("adb devices");
BufferedReader in = new BufferedReader(new InputStreamReader(
proc.getInputStream()));
String line = null;
while ((line = in.readLine()) != null) {//遍历所有的设备id
if ((line.contains("device") || line.contains("offline")) && !line.contains("devices")) { //排除第一行和最后一行的数据
String deviceId = line.replace("device", "").trim();
if (line.contains("device")) {
deviceId = line.replace("device", "").trim();
}
if (line.contains("offline")) {
deviceId = line.replace("offline", "").trim();
}
startActivityByJava(deviceId);
}
}
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
static class MyTask1 extends java.util.TimerTask {
public void run() {
int which = new Random().nextInt(activitys.size());
System.out.println("force jump activity:" + activitys.get(which));
activityPaths[0] = pakageName + "/." + activitys.get(which);
util.setActivityPaths(activityPaths);
util.forceJumpActivity();
}
}
- 参数配置
以main函数作为入口,根据项目的需求定义四个参数
/**
* @param args
*/
public static void main(String[] args) { //pakageName pythonPath runnerPath activityPaths
// TODO Auto-generated method stub
pakageName = args[0];
String pythonPath = args[1];
String runnerPath = args[2];
String json = args[3];
List list = new ArrayList<>();
JSONArray array = JSONArray.fromObject(json);
String activityPaths[] = new String[array.size()];
for (int i = 0; i < array.size(); i++) {
JSONObject object = (JSONObject) array.get(i);
ActivityConfigureBean bean = (ActivityConfigureBean) JSONObject.toBean(object, ActivityConfigureBean.class);
System.out.println("activity=" + bean.getActivity());
list.add(bean);
for (int j = 0; j < bean.getScale(); j++) {
activitys.add(bean.getActivity());
}
activityPaths[i] = pakageName + "/." + bean.getActivity();
System.out.println("bean.activity=" + bean.getActivity());
}
util = new MonkeyRunnerUtil.Builder().pakageName(pakageName).pythonPath(pythonPath).runnerPath(runnerPath).activityPaths(activityPaths).build();
Timer timer = new Timer();
timer.schedule(new MyTask(), 1000, 5000); //5秒一次监听跳出
timer.schedule(new MyTask1(), 1000, totalTime /activityPaths.length); //指定时间间隔跳转指定页面 时间间隔由配置的页面数确定
// util.startActivityByJava("3483170");
// util.startActivityByPython(util.runnerPath,util.pythonPath);
// System.out.println("isAlive="+util.watchActivity());
}
其中,
pakagename:所要运行程序的包名
pythonpath:python脚本在本地的路径 一般为空字符串
runnerpath:monkeyrunner在本地的路径 一般为空字符串
json:页面的覆盖率配置,需要严格的json数据格式,如:“[ { \”activity\”: \”MainActivity\”, \”scale\”: 1 }
, { \”activity\”: \”active.me.LoginActivity\”, \”scale\”: 2 }, { \”activity\”: \”LoadingActivity\”, \”scale\”: 3
}]”,activity指页面,scale指权重
static class MyTask extends java.util.TimerTask {
public void run() {
// int which = new Random().nextInt(activitys.size());
System.out.println("check out");
// activityPaths[0] = pakageName + "/." + activitys.get(which);
// util.setActivityPaths(activityPaths);
util.checkForStartActivity(); //监听跳出则跳转指定的页面
}
}
static class MyTask1 extends java.util.TimerTask {
public void run() {
int which = new Random().nextInt(activitys.size()); //随机取数 以达到不同的覆盖比例
System.out.println("force jump activity:" + activitys.get(which));
activityPaths[0] = pakageName + "/." + activitys.get(which);
util.setActivityPaths(activityPaths);
util.forceJumpActivity();
}
}
- 导出成jar
项目是以Android studio中Module为Android Library的形式存在,导出jar包:
首先,Module下的build文件中配置
task makeJar(type: Copy) {
delete 'build/libs/cmdlib.jar'
from('build/libs/')
into('build/libs/')
include('cmdlib.jar')
rename ('cmdlib.jar', 'monkeycontroller.jar')
}
makeJar.dependsOn(build)
接着通过终端指令 gradlew makeJar 可导出jar包
此jar当前还不能使用java -jar 直接使用,还需要两步操作
(1) 拷贝出jar包以及项目中的libs文件夹到pc本地的同一目录下
(2) 利用WinRAR找到清单文件并修改更新。注意清单文件的修改规范,最后必须有两行以上的空行,否则无法识别Class-Path
此时的jar便可以通过java -jar的指令使用了
monkeycontroller.jar的使用
开启monkey
打开cmd指令窗口,执行指令例如
adb shell monkey -p org.sojex.finance -s 500 –throttle 1000 –ignore-crashes –ignore-timeouts -v 10000000开启monkeycontroller
打开一个新的cmd指令窗口,cd到monkeycontroller.jar的目录下
执行指令例如
java -jar monkeycontroller.jar <”pakagename”> <”pythonpath”> <”runnerpath”> <”json”>
其中
pakagename:所要运行程序的包名
pythonpath:python脚本在本地的路径 一般为空字符串
runnerpath:monkeyrunner在本地的路径 一般为空字符串
json:页面的覆盖率配置,需要严格的json数据格式,参考实例:
java -jar monkeycontroller.jar org.sojex.finance “” “” “[ { \”activity\”: \”MainActivity\”, \”scale\”: 1 }
, { \”activity\”: \”active.me.LoginActivity\”, \”scale\”: 2 }, { \”activity\”: \”LoadingActivity\”, \”scale\”: 3
}]”
其中
json字符串中,key为 “activity” 的 value 对应为activity的名称,key为 “scale” 对应的 value 为需要启动的权重比注意事项
参数顺序不可变,严格参照上述实例顺序,其中pythonpath和runnerpath如果没有相关资源必须以空字符串占位