ChimpChat.jar加adb shell实现类似QTP的录制/回放/断言自动化测试系统模型
因为项目调整的关系,最近的工作方向转向C,linux和信息通信这块(之前的软肋,边补边学边做吧),很久没碰Android和Java,以及测试相关的东西了,所以得空把半年前开始做的一些Android相关的自动化测试研究整理了一下,也算不上研究,只是个人的一些想法,觉得还有很多可以扩展的地方,如果遗忘了的话着实有些可惜。
相比较Android测试来说,Windows程序测试以及网页测试都有比较完善和成熟的脚本录制/回放框架,操作的步骤大概如下:
1. 人工录制脚本,通常为一个可重复的测试用例步骤。
2. 生成和分析脚本文件,可以人为的修改,增,删,调整顺序等。
3. 在脚本的步骤中添加断言,提供指定控件的预期值。
4. 执行脚本,比较预期值,生成测试报告。
还有一些其他的功能,比如脚本的批量管理,测试计划的安排等等,这里就不多说了。
前一阵子EOE发布了一款Itestin的工具,号称可以实现录制/回放和云测试的功能,我没有用过这个工具,不过从其他人的反馈来看,效果并不是很理想,回放的效果很差。而且这类测试工具面向的对象是App开发商,关注的是app在不同型号手机上运行脚本的情况,而作为手机生产商,关心的是一款手机上的系统App 和第三方的App的运行情况,稳定和可回归是我们更关注的东西。既然没什么好办法,就自己试着弄一个吧,而且整个程序是通过Java来实现。
A. 录制
录制其实是最难搞的地方,如何在不修改系统本身的情况下获取到屏幕和按钮的各种事件?像Robotium一类基于Instrumentation机制的工具,通过获取UI线程来直接控制控件,虽然比较精确,但最大的缺点在于无法进行跨模块的调用。而我们对录制的精确程度要求并不高,重要的在于能够精确回访,于是就想到了adb shell getevent。
在命令行下输入adb shell getevent -p可以查看手机各个内部设备的信息,下图是某样机的触屏设备信息,触屏设备号为/dev/input/event2(有些样机可能为event3或其他)。

注意0035和0036开头的属性,这两个分别代表了长(Y轴)和宽(X轴),实际在开发的过程中发现,触屏的Y轴长度是超过800的,还包含了屏幕下面的四个按键部分。
指令adb shell getevent /dev/input/event2会显示所有的触屏指令,以一次点击事件为例,指令的意思可以见注释:
SingleLine:0001 014a 00000001 //Touch Down,手指按下 SingleLine:0003 0000 000000ef SingleLine:0003 0001 000002f8 SingleLine:0003 0018 00000031 SingleLine:0003 001c 00000006 SingleLine:0003 003a 00000031 SingleLine:0003 0035 000000ef //X轴坐标,显示为16进制 SingleLine:0003 0036 000002f8 //Y轴坐标,显示为16进制 SingleLine:0003 0030 00000031 SingleLine:0003 0039 00000000 SingleLine:0000 0002 00000000 SingleLine:0000 0000 00000000 //这两行为一次指令间隔,单击事件中,手指按下的时候,焦点其实会发生小范围的变化,所以会有多个X,Y对 SingleLine:0003 0000 000000ee SingleLine:0003 0001 000002fa SingleLine:0003 0018 00000038 SingleLine:0003 003a 00000038 SingleLine:0003 0035 000000ee SingleLine:0003 0036 000002fa SingleLine:0003 0030 00000038 SingleLine:0003 0039 00000000 SingleLine:0000 0002 00000000 SingleLine:0000 0000 00000000 SingleLine:0003 0000 000000ef SingleLine:0003 0018 00000039 SingleLine:0003 003a 00000039 SingleLine:0003 0035 000000ef SingleLine:0003 0036 000002fa SingleLine:0003 0030 00000039 SingleLine:0003 0039 00000000 SingleLine:0000 0002 00000000 SingleLine:0000 0000 00000000 SingleLine:0003 0000 000000ee SingleLine:0003 0001 000002f9 SingleLine:0003 0018 00000038 SingleLine:0003 003a 00000038 SingleLine:0003 0035 000000ee SingleLine:0003 0036 000002f9 SingleLine:0003 0030 00000038 SingleLine:0003 0039 00000000 SingleLine:0000 0002 00000000 SingleLine:0000 0000 00000000 SingleLine:0003 0000 000000ea SingleLine:0003 0001 000002f8 SingleLine:0003 0018 00000030 SingleLine:0003 001c 00000005 SingleLine:0003 003a 00000030 SingleLine:0003 0035 000000ea SingleLine:0003 0036 000002f8 SingleLine:0003 0030 00000030 SingleLine:0003 0039 00000000 SingleLine:0000 0002 00000000 SingleLine:0000 0000 00000000 SingleLine:0001 014a 00000000 //Touch Up,手指松开 SingleLine:0000 0002 00000000 SingleLine:0000 0000 00000000
Drag,94,467,367,453 Wait,3775 Touch,148,746 Wait,3104 Touch,445,757 Wait,2060 Long,233,457
B. 回放
回放的选择就多种多样了,这里主要运用了ChimpChat.jar这个包,Monkeyrunner在实现点击,拖拽时也是调用的这个包,实现原理是与设备中的Monkey Server进行交互,以C/S的方式发送指令,由Monkey Server进行响应。但Monkeyrunner的录制程序是将DUT屏幕映射到程序中的,因为要缓存图片,导致整个程序相当之卡,非常影响操作。在源码的sdk目录下面有ChimpChat和monkeyrunner(是用Jython写的,除去和python相关的参数获取过程,还是比较好读的)的源码,有兴趣的可以去看一看。
import com.android.chimpchat.ChimpChat;
import com.android.chimpchat.core.IChimpDevice;
import com.android.chimpchat.core.PhysicalButton;
import com.android.chimpchat.hierarchyviewer.HierarchyViewer;
…………
//获取IChimpDevice,大部分的回放操作要通过该device进行
ChimpChat chimp = ChimpChat.getInstance();
IChimpDevice device = chimp.waitForConnection();
…………
//点击
private static void performClick(IChimpDevice device, int x, int y) throws IOException,
InterruptedException {
device.getManager().touch(x, y);
}
//拖动
private static void performDrag(IChimpDevice device, int startX, int startY, int endX,
int endY, int steps, long time) throws InterruptedException {
device.drag(startX, startY, endX, endY, steps, time);
}
//发送Android四个按键事件,因为adb shell getevent获取的是整个触控板的底层响应,所以要根据坐标范围将点击事件转换为相应的按钮事件
private static void performPressButton(IChimpDevice device, PhysicalButton button)
throws IOException, InterruptedException {
device.getManager().press(button);
}
//长按
private static void performLongClick(IChimpDevice device, int x, int y) throws IOException,
InterruptedException {
device.getManager().touchDown(x, y);
}
//等待
private static void performWait(IChimpDevice device, long millTime) throws InterruptedException {
Thread.sleep(millTime);
}
C.断言
实现录制/回放后,自动化的程度还不算高,还需要在测试步骤中加入断言,即指定某个控件和它的预期值,在回放的过程中进行读取和判断。大家可以去看下Hierarchyviewer的实现原理,有提到通过一个控件的ID获取到他的文本值得方法,可喜可贺的是ChimpChat.jar包中已经封装了相关的方法,比如类似于getText(device.findViewById("id/btn_ok"))。如果大家对 HierarchyViewer的实现原理还有印象的话,可以发现这个getText方法,其实就是通过ID查找DUMP出的所有ViewNode节点中的" text:mText" 字段。需要注意的是SDK提供的HierarchyViewerLib.jar包在与手机设备端口交互时,并没有使用UTF-8的格式,所以需要修改源码重新打包,不然会无法识别中文。
具体实现时,可以通过修改ChimpChat.jar包中的Hierarchyviewer.java类来添加相关的断言方法(重新打包或者将源码复制到工程中),该类是ChimpChat对Hierarchy包的一个封装,里面有不少值得参考的API。
可以自己添加和修改相关的API,比如显示当前屏幕所有的控件坐标和文本信息:
/* List current window's viewNode info. */
public void listViewId() {
ViewNode rootNode = DeviceBridge.loadWindowData(
new Window(mDevice, "", 0xffffffff));
if (rootNode == null) {
throw new RuntimeException("Could not dump view");
}
listViewId(rootNode);
}
public void listViewId(ViewNode node) {
for (ViewNode child : node.children) {
System.out.println("ID:" + child.id + ", Text:" + getText(child) + ", Name:"
+ child.name
+ ", X:" + getAbsoluteCenterOfView(child).x + ", Y:"
+ getAbsoluteCenterOfView(child).y + ", Visible:" + visible(node));
listViewId(child);
}
}
查找当前界面可见的没有被隐藏的文本:
/* Search string in current visible viewNode. */
public boolean searchText(String str) {
ViewNode rootNode = DeviceBridge.loadWindowData(new Window(mDevice, "", 0xffffffff));
if (rootNode == null) {
throw new RuntimeException("Could not dump view");
}
return searchText(str, rootNode);
}
public boolean searchText(String str, ViewNode node) {
if (visible(node) && getText(node).contains(str)) {
return true;
}
for (ViewNode child : node.children) {
if (searchText(str, child)) {
return true;
}
}
return false;
}
需要说明一下visible(node)这个函数,原本的visible函数只会判断当前的控件是否是visible的状态,但对于一些复杂的界面来说,比如原生联系人的三个界面(群组,联系人和收藏)是写在同一个布局文件中,当处于一个界面时,其他两个界面的控件也是处于visible的状态,所以还需要加上另一个判断——x,y轴的坐标都大于0,道理比较简单,获取的x,y轴坐标都是相对于当前屏幕坐标系的坐标,在当前界面之外的控件要么是x轴,要么是y轴,存在着负值。
/**
* Gets the visibility of a given element.
*
* @param selector selector for the view.
* @return True if the element is visible.
*/
public boolean visible(ViewNode node) {
boolean ret = (node != null)
&& node.namedProperties.containsKey("getVisibility()")
&& "VISIBLE".equalsIgnoreCase(
node.namedProperties.get("getVisibility()").value)
&& getAbsolutePositionOfView(node).x >= 0
&& getAbsolutePositionOfView(node).y >= 0;
return ret;
}
/**
* Gets the text of a given element.
*
* @param selector selector for the view.
* @return the text of the given element.
*/
public String getText(ViewNode node) {
if (node == null) {
throw new RuntimeException("Node not found");
}
ViewNode.Property textProperty = node.namedProperties.get("text:mText");
if (textProperty == null) {
return "";
}
return textProperty.value;
}
通过API还可以实现一个比较有意思的功能,通过坐标值来获取控件名和文本,是不是觉得之前获得的脚本文件都是坐标一点都不容易理解,那可以通过某些“翻译”,将坐标转换为控件名等属性。实现起来也比较简单,一个布局文件就是一个控件树,根据坐标找到最底层的节点即可。
public ViewNode findViewByCoordinate(int x, int y) {
ViewNode rootNode =DeviceBridge.loadWindowData(new Window(mDevice,"",0xffffffff));
if (rootNode == null) {
throw new RuntimeException("Could not dump view");
}
return findViewByCoordinate(x, y, rootNode);
}
public ViewNode findViewByCoordinate(int x, int y, ViewNode node) {
if (hasChild(node)) {
for (ViewNode child : node.children) {
findViewByCoordinate(x, y, child);
}
}
if (inView(x, y, node)) {
return node;
}
return null;
}
private boolean inView(int x, int y, ViewNode node) {
Point point = getAbsolutePositionOfView(node);
return (x > point.x) && (x < point.x + node.width) && (y > point.y)
&& (y < point.y + node.height);
}
private boolean hasChild(ViewNode node) {
return node.children.isEmpty();
}
录制,回放,断言三个部分解决后,就可以开始一些简单的测试,之前是想做成Eclipse插件的形式,用swt+jface做界面,后来做了个开头就转到其他项目了,所以只好暂时搁置下来,其实距离一个完善的系统还有很长的距离,包括脚本管理,批量执行,log生成,以及ChimpChat,Hierarchy这些库都是通过C/S和机器中的MonkeyService进行通信,执行脚本和断言间可能会存在通信阻塞等问题(实验中碰到一两回),暂时先挖个坑在这边吧。

浙公网安备 33010602011771号