HTML脚本配置Android自动化测试

说明

  在项目配置完基于robotium框架下的自动化测试用例后发现虽然用代码配置测试用例虽然较为灵活,但是如果编写较为全面的测试用例则必然会消耗大量开人员的精力,并且对于用例的后期维护也是很大一部分投入,使开发人员无法更为专注于项目构建,如此萌生了将测试用例以HTML脚本方式进行表述,可让测试人员进行配置测试脚本,测试人员在进行全流程业务测试过程中进行自动化测试脚本的编写,并且可进行增量维护,在项目上线前可以用最小的时间代价进行最全面的测试工作,对于项目质量的把控有不可忽视的作用。

  本自动化测试项目需要开发人员针对项目进行一定的配置,当配置完成后可对自动化测试项目进行打包,与被测项目安装在同一设备内,通过adb执行指定命令完成自动化测试的流程。除非有重大变,自动化测试项目仅需开发一次,可针对同一被测项目重复使用,所有测试流程的设备行为由HTML标签进行约束,测试人员以约定HTML标签编写测试用例,可动态指定执行的测试用例,便于测试用例的维护。

执行环境:

1、被测试项目安装包

2、自动化测试项目安装包

3、Android开发环境(示例开发工具为eclipse)

4、Android设备

自动化测试项目初始构建

1、新建被测试项目,并编写相关功能。

2、创建测试项目,关联被测试项目。

3、测试项目导入robotium-solo jar包(可于官网下载)

4、编写代码自动化测试用例,测试robotium自动化测试是否可正常执行

开发工具中执行测试用例:光标放在当前类中,点击鼠标右键 --> RunAs --> Android Junit Test


1、创建一个Android项目,编辑项目清单文件

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:andro
    package="com.walker.autotest"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="9" />
        <!-- 必须指定,并设置目标项目包名:com.waitingfy.iosunlock 替换成目标项目的包名-->
    <instrumentation
        android:name="android.test.InstrumentationTestRunner"
        android:targetPackage="com.waitingfy.iosunlock" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        
        <!-- 必须指定所用library(拷贝即可) -->
        <uses-library android:name="android.test.runner" />
        
    </application>

</manifest>

 2、 增加自动化测试用例执行入口,代码如下:
/**   
* @Title: HtmlScariptTest.java
* @Package com.walker.autotest
* @Description: TODO
* @author A18ccms A18ccms_gmail_com   
* @date 2017年9月4日 下午2:00:09
* @version V1.0   
*/
package com.walker.autotest;

import com.robotium.solo.Solo;

import android.os.Environment;
import android.test.ActivityInstrumentationTestCase2;
import android.util.Xml;

/** @ClassName: HtmlScariptTest
 *
 * @Description: TODO
 *
 * @author walker
 *
 * @date 2017年9月4日 下午2:00:09
 * 
 */
public class HtmlScariptTest extends ActivityInstrumentationTestCase2{
        Solo solo;
        String logFileName = "testLog.txt";
        private static Class<?> launchActivityClass;
        //被测试应用入口界面Activity全名
        private static String mainActiviy = "com.waitingfy.iosunlock.MainActivity";
        //被测试应用包名
        private String packageName = "com.waitingfy.iosunlock";
        //加载入口Activity,获取Activity的类对象
        static {
                try {
                        launchActivityClass = Class.forName(mainActiviy);
                } catch (ClassNotFoundException e) {
                        throw new RuntimeException(e);
                }
        }
        
        /**
         * 默认构造函数,以入口Activity对象为参数调用父类构造,设定测试用例入口执行逻辑
         */
        public HtmlScariptTest() {
                super(launchActivityClass);
        }
        /**
         * 测试用例初始化时设置solo对象,对设备的所有操作通过solo对象进行。
         */
        @Override
        protected void setUp() throws Exception {
                super.setUp();
                solo = new Solo(getInstrumentation(),getActivity());
        }
        //必须覆写
        @Override
        protected void tearDown() throws Exception {
                super.tearDown();
        }
        /**
         * Add by walker Date 2017年9月4日
         * @Description: TODO
         *      测试用例执行入口,测试用例方法名以test开头命名
         */
        public void test0(){
                solo.sleep(10000);
        }

标签设计

标签动作属性
testCase指定测试用例测试用例文件名
data标记内容为测试数据dataType各标签值
clickView点击指定ID的View控件ID,hasText
inputText指定编辑框录入内容ID输入框录入内容
checkBox设置指定选择框的勾选属性ID值为1勾选,否则不勾选
spinner列表选择ID被选择内容的文本值
sleep延迟时间后再继续执行延时时长(毫秒)
onKeyDown键盘按键事件按键值,支持范围如下
clickOnText点击显示的文本hasText被点击的文本字符串
takeScreenshot截屏并保存图片
waitForActivity等待指定页面指定页面的activity值
checkData校验数据标签tableName
filed数据校验字段filedName指定字段的值

testCase:仅在EMS.xml中起作用,且Main.xml文件中只识别testCase标签

data:包裹测试数据,被包裹内容为测试脚本数据,属性dataType来设定数据类型。

clickView:点击页面View控件,属性有ID、hasText。ID属性不可为空,以ID值进行索引View控件并进行点击;hasText属性值可以为空,如果hasText为空或者在当前可见页面可以所搜到此文本则执行点击事件。

clickOnText:点击页面上指定的文本,属性有hasText。如果hasText为空或者在当前可见页面可以搜索到此文本则执行点击事件。

waitForActivity:值为页面Activity名称,等待指定的Activity,如果不是指定activity则结束测试流程

spinner:点击控件后显示候选列表,并从列表中选择标签指定的值。

takeScreenshot:截图文件保存路径(/sdcard/Robotium-Screenshots)

checkData:用于指定校验数据,属性tableName为被查询的表名,标签包裹内容为字段属性值

filed:用于指定被校验表的字段属性,属性filedName指定字段名称,值为字段的值。此标签应被checkData标签包裹。

支持的按键:back(回退键)、enter(确认键)、left(左键)、right(右方向键)、up(上方向键)、down(下方向键)、数字(1-9);
ID属性:为项目中xml文件中配置id值,可通过项目源码获取

代码解析HTML标签配置的测试用例

  如下代码为带有HTML标签解析的代码,在执行测试用例时首先对指定路径下的配置文件main.xml进行读取,根据其配置的脚本文件加载对应的执行脚本,然后依次执行所配置的脚本,完成自动化测试流程。


/**   
* @Title: HtmlScariptTest.java
* @Package com.walker.autotest
* @Description: TODO
* @author A18ccms A18ccms_gmail_com   
* @date 2017年9月4日 下午2:00:09
* @version V1.0   
*/
package com.walker.autotest;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.xmlpull.v1.XmlPullParser;

import com.robotium.solo.Solo;

import android.os.Environment;
import android.test.ActivityInstrumentationTestCase2;
import android.util.Xml;
import android.view.KeyEvent;
import android.view.View;
import android.widget.CheckBox;
import android.widget.EditText;

/**
 * @ClassName: HtmlScariptTest
 *
 * @Description: TODO
 *
 * @author walker
 *
 * @date 2017年9月4日
 * 
 */
public class HtmlScariptTest extends ActivityInstrumentationTestCase2 {
        Solo solo;
        String logFileName = "testLog.txt";
        private static Class<?> launchActivityClass;
        // 被测试应用入口界面Activity全名
        private static String mainActiviy = "com.waitingfy.iosunlock.MainActivity";
        // 被测试应用包名
        private String packageName = "com.waitingfy.iosunlock";

        // 加载入口Activity,获取Activity的类对象
        static {
                try {
                        launchActivityClass = Class.forName(mainActiviy);
                } catch (ClassNotFoundException e) {
                        throw new RuntimeException(e);
                }
        }

        /**
         * 默认构造函数,以入口Activity对象为参数调用父类构造,设定测试用例入口执行逻辑
         */
        public HtmlScariptTest() {
                super(launchActivityClass);
        }

        /**
         * 测试用例初始化时设置solo对象,对设备的所有操作通过solo对象进行。
         */
        @Override
        protected void setUp() throws Exception {
                super.setUp();
                solo = new Solo(getInstrumentation(), getActivity());
        }

        @Override
        protected void tearDown() throws Exception {
                super.tearDown();
        }

        /**
         * Add by walker Date 2017年9月4日
         * 
         * @Description: TODO 测试用例执行入口,测试用例方法名以test开头命名
         */
        public void test0() {
                loadScriptList();
        }

        String configPath = "";

        /**
         * Add by walker Date 2017年8月29日
         * 
         * @Description: TODO 加载解析脚本清单配置文件配置脚本文件
         */
        private void loadScriptList() {
                configPath = Environment.getExternalStorageDirectory() + "/Test/";
                File file = new File(configPath + "Main.xml");
                try {
                        InputStream in = new FileInputStream(file);
                        XmlPullParser parser = Xml.newPullParser();
                        parser.setInput(in, "utf-8");
                        int type = parser.getEventType();
                        while (type != XmlPullParser.END_DOCUMENT) {
                                String value = "";
                                switch (type) {
                                case XmlPullParser.START_TAG:
                                        value = parser.nextText();
                                        if ("testCase".equals(parser.getName())) {
                                                // 读取脚本文件,加载执行所配置的脚本文件
                                                xmlLoad(value);
                                        }
                                        break;
                                case XmlPullParser.END_TAG:
                                        break;
                                }
                                type = parser.next();
                        }
                } catch (Exception e) {
                        e.printStackTrace();
                }

        }

        /** 数据标志:1-data类型; 2-checkData */
        int dataFlag = 0;
        HashMap<String, String> dataMap = new HashMap<String, String>();
        HashMap<String, String> lableAction = new HashMap<String, String>();

        /**
         * Add by walker Date 2017年8月29日
         * 
         * @Description: TODO 加载自动化测试脚本,并执行自动化测试
         * @param fileName
         *            自动化测试脚本名称
         */
        public void xmlLoad(String fileName) {
                File file = new File(configPath + fileName);
                try {
                        InputStream in = new FileInputStream(file);
                        // 创建xmlPull解析器
                        XmlPullParser parser = Xml.newPullParser();
                        /// 初始化xmlPull解析器
                        parser.setInput(in, "utf-8");
                        // 读取文件的类型
                        int type = parser.getEventType();
                        int depth = parser.getDepth();
                        String id = "";
                        String value = "";
                        String hasText = "";
                        String dataType = "";
                        String filedName = "";
                        String tableName = "";
                        ArrayList<Map<String, String>> actionList = new ArrayList<Map<String, String>>();
                        while ((type != XmlPullParser.END_TAG || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
                                id = "";
                                value = "";
                                hasText = "";
                                switch (type) {
                                // 开始标签
                                case XmlPullParser.START_TAG:
                                        id = parser.getAttributeValue(null, "id");
                                        hasText = parser.getAttributeValue(null, "hasText");
                                        filedName = parser.getAttributeValue(null, "filedName");
                                        if ("data".equals(parser.getName())) {
                                                actionList = new ArrayList<Map<String, String>>();
                                                dataFlag = 1;
                                                dataType = parser.getAttributeValue(null, "dataType");
                                                break;
                                        }
                                        if ("checkData".equals(parser.getName())) {
                                                actionList = new ArrayList<Map<String, String>>();
                                                dataMap = new HashMap<String, String>();
                                                tableName = parser.getAttributeValue("", "tableName");
                                                dataMap.put("dataName", tableName);
                                                dataFlag = 2;
                                                break;
                                        }
                                        value = parser.nextText();
                                        lableAction = new HashMap<String, String>();
                                        lableAction.put("dataType", dataType);
                                        lableAction.put("ID", id);
                                        lableAction.put("hasText", hasText);
                                        lableAction.put("tableName", tableName);
                                        lableAction.put("filedName", filedName);
                                        lableAction.put("value", value);
                                        lableAction.put("actionName", parser.getName());
                                        actionList.add(lableAction);
                                        if (dataFlag == 2 && !isEmptyUnNull(filedName) && !dataMap.containsKey(filedName)) {
                                                dataMap.put(filedName, value);
                                        } else {
                                        }

                                        if ("clickView".equals(parser.getName())) {// 按钮
                                                if (isEmptyUnNull(hasText) || solo.searchText(hasText, true)) {
                                                        clickOnView(id);
                                                }
                                        } else if ("inputText".equals(parser.getName())) {// 文本输入框
                                                enterText(id, value);
                                        } else if ("checkBox".equals(parser.getName())) {// 复选框
                                                if ("1".equals(value)) {
                                                        chkSelect(id, true);
                                                } else {
                                                        chkSelect(id, false);
                                                }
                                        } else if ("spinner".equals(parser.getName())) {// 下拉选择
                                                selectData(value, id);
                                        } else if ("sleep".equals(parser.getName())) {// 睡眠时间
                                                try {
                                                        sleep(Integer.parseInt(value) / 1000);
                                                } catch (Exception e) {
                                                        e.printStackTrace();
                                                }
                                        } else if ("onKeyDown".equals(parser.getName())) {// 按键事件
                                                sendKey(value);
                                        } else if ("clickOnText".equals(parser.getName())) {// 点击文本
                                                solo.clickOnText(value + "");
                                        } else if ("takeScreenshot".equals(parser.getName())) {// 屏幕拍照
                                                solo.takeScreenshot();
                                                solo.sleep(300);
                                        } else if ("waitForActivity".equals(parser.getName())) {
                                                sleep(2);
                                                if (solo.waitForActivity(value)) {
                                                        break;
                                                } else {
                                                        return;
                                                }
                                        }
                                        break;
                                case XmlPullParser.END_TAG:
                                        if ("data".equals(parser.getName())) {
                                                dataType = "";
                                        } else if ("checkData".equals(parser.getName())) {
                                                sleep(1);
                                                checkData();
                                        }
                                        break;
                                }
                                type = parser.next();
                        }
                } catch (Exception e) {
                        e.printStackTrace();
                }
        }

        /**
         * Add by walker Date 2017年9月1日
         * 
         * @Description: TODO 根据配置文件校验数据
         */
        private void checkData() {
                StringBuilder sql = new StringBuilder();
                sql.append("select * from ");
                if (dataMap != null && dataMap.size() > 1 && !isEmptyUnNull(dataMap.get("dataName"))) {
                        sql.append(dataMap.get("dataName") + " where ");
                        Set<Entry<String, String>> set = dataMap.entrySet();
                        Iterator<Entry<String, String>> it = set.iterator();
                        Map.Entry<String, String> me;
                        while (it.hasNext()) {
                                me = it.next();
                                if ("dataName".equals(me.getKey())) {
                                } else {
                                        sql.append(me.getKey() + "=\'" + me.getValue() + "\' and ");
                                }
                        }
                        if ((sql + "").endsWith("and ")) {
                                sql.delete(sql.lastIndexOf("and"), sql.length());
                        }
                        // 校验数据逻辑,可根据具体项目进行配置,执行sql查询语句,获取查询结果,如在指定条件下获取查询结果为空,那么
                        HashMap<String, String> data = null;
                        // data = DbUtils.getInstance().queryFirstData(sql +"");
                        if (data != null && data.size() > 0) {
                        } else {
                        }
                } else {
                        return;
                }
        }

        /**
         * Add by walker Date 2017年9月4日
         * 
         * @Description: TODO 延时器
         * @param time
         *            延时时间,单位为秒
         */
        private void sleep(int time) {
                long start = System.currentTimeMillis();
                while ((System.currentTimeMillis() - start) < time * 1000) {
                }
        }

        /**
         * Add by walker Date 2017年9月4日
         * 
         * @Description: TODO 判断字符串是否为空
         * @param value
         *            字符串的值
         * @return 如果字符串为null或""或"null"或者全部为空白字符则返回true,否则返回false。
         */
        public static boolean isEmptyUnNull(String value) {
                if (value != null && !"".equalsIgnoreCase(value.trim()) && !"null".equalsIgnoreCase(value.trim())
                                && value.trim().length() != 0) {
                        return false;
                } else {
                        return true;
                }
        }

        /**
         * Add by walker Date 2017年9月4日
         * 
         * @Description: TODO 模拟按键事件,按键前后进行延时
         * @param keyStr
         *            按键字符
         *            支持的按键:back(回退键)、enter(确认键)、left(左键)、right(右方向键)、up(上方向键)、down(
         *            下方向键)、数字(1-9)
         */
        public void sendKey(String keyStr) {
                int key = 0;
                if ("1".equals(keyStr)) {
                        key = KeyEvent.KEYCODE_1;
                } else if ("3".equals(keyStr)) {
                        key = KeyEvent.KEYCODE_3;
                } else if ("4".equals(keyStr)) {
                        key = KeyEvent.KEYCODE_4;
                } else if ("5".equals(keyStr)) {
                        key = KeyEvent.KEYCODE_5;
                } else if ("6".equals(keyStr)) {
                        key = KeyEvent.KEYCODE_6;
                } else if ("7".equals(keyStr)) {
                        key = KeyEvent.KEYCODE_7;
                } else if ("8".equals(keyStr)) {
                        key = KeyEvent.KEYCODE_8;
                } else if ("9".equals(keyStr)) {
                        key = KeyEvent.KEYCODE_9;
                } else if ("back".equals(keyStr)) {
                        key = KeyEvent.KEYCODE_BACK;
                } else if ("enter".equals(keyStr)) {
                        key = KeyEvent.KEYCODE_DPAD_CENTER;
                } else if ("left".equals(keyStr)) {
                        key = KeyEvent.KEYCODE_DPAD_LEFT;
                } else if ("right".equals(keyStr)) {
                        key = KeyEvent.KEYCODE_DPAD_RIGHT;
                } else if ("up".equals(keyStr)) {
                        key = KeyEvent.KEYCODE_DPAD_UP;
                } else if ("down".equals(keyStr)) {
                        key = KeyEvent.KEYCODE_DPAD_DOWN;
                }
                solo.sleep(30);
                solo.sendKey(key);
                solo.sleep(30);
        }

        /**
         * Add by walker Date 2017年9月4日
         * 
         * @Description: TODO 选择对应信息
         * @param selectedStr
         *            选择的类型(文本内容)
         * @param id
         *            下拉选择控件ID
         * @param title
         *            选择框的标题字符串
         * @param ranges
         *            被选择范围-所选择的字符串必须在此范围内
         */
        public void selectData(String selectedStr, String id) {
                clickOnView(id);
                solo.sleep(1000);
                solo.clickOnText(selectedStr);
        }

        /**
         * Add by walker Date 2017年9月4日
         * 
         * @Description: TODO 点击ID所指定的控件
         * @param viewId
         *            视图控件ID
         */
        public void clickOnView(String viewId) {
                try {
                        View view = getView(solo, viewId);
                        solo.clickOnView(view);
                } catch (Exception e) {
                        e.printStackTrace();
                }
        }

        public View getView(Solo solo, String idStr) {
                int id = solo.getCurrentActivity().getResources().getIdentifier(idStr, "id", packageName);
                View v = solo.getView(id);
                return v;
        }

        /**
         * Add by walker Date 2017年9月4日
         * 
         * @Description: TODO 勾选框勾选功能
         * @param id
         *            checkBox控件ID
         * @param checked
         *            设置是否选择此控件
         */
        public void chkSelect(String id, final boolean checked) {
                CheckBox chk = (CheckBox) solo.getView(id);
                if (!chk.isChecked() && checked) {
                        solo.clickOnView(chk);
                }
        }

        /**
         * Add by walker Date 2017年9月4日
         * 
         * @Description: TODO 为文本录入框录入字符串,文本录入完成后延迟指定时间
         * @param id
         *            文本录入框ID
         * @param msg
         *            文本录入信息
         */
        public void enterText(String id, String msg) {
                EditText editText = (EditText) getView(solo, id);
                if (editText != null) {
                        solo.clickOnView(editText);
                        solo.clearEditText(editText);
                        solo.enterText(editText, msg + "");
                }
                solo.sleep(200);
        }
}

脚本编写

1、新建main.xml配置文件,根据标签testCase指定测试脚本文件。

2、根据动作标签编写测试脚本用例,以main.xml中指定的文件名为名。

3、将main.xml文件及脚本配置文件放到Android设备sd卡下Test文件夹下(如果文件夹不存在,那么新建此文件夹,文件名区分大小写)

执行脚本

  在目标Android设备安装上测试apk及被测试apk签名后在电脑命令行上执行如下命令即可实现自动化测试启动流程。


adb shell am instrument -e class com.walker.autotest.HtmlScariptTest -w com.walker.autotest/android.test.InstrumentationTestRunner 

注意事件

1、测试apk安装包与被测试项目安装包apk的应用签名要一致。

2、测试项目如果引用库工程或者V4包时需要注意有无重复的包,本用例编写时测试工程引用了V4包,导致被测应用启动失败。