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
 
  比较重要的就是0001 014a 00000000和0001 014a 00000001这两个响应,可以把它们作为不同动作间的分隔符,类似的也可以识别出长按,拖动等操作。
  还有一点要注意的是,为了精确地模拟手动输入的间隔时间,需要同时记录按下和松开这两个指令的时间戳,在生成脚本文件时通过比对时间戳,算出操作之间的间隔,在回放时确保操作的时间间隔也保持一致。这样,通过获取起始坐标和结束坐标,比对间隔时间和距离,可以判断出点击操作、拖动操作、长按操作。不过这只是比较简单的判断方法,应该有更准确的数学模型来分析输出的log,这是需要改进的一个地方。
  翻译后的脚本文件如下:
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进行通信,执行脚本和断言间可能会存在通信阻塞等问题(实验中碰到一两回),暂时先挖个坑在这边吧。

posted @ 2012-12-24 22:41  小甚  Views(2235)  Comments(2)    收藏  举报