说明

  在项目配置完基于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:android="http://schemas.android.com/apk/res/android"
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包,导致被测应用启动失败。

HTML脚本配置Android自动化测试的更多相关文章

  1. 解放双手——Android自动化测试

    解放程序猿宝贵的右手(或者是左手) http://blog.csdn.net/eclipsexys/article/details/45622813 --Android自动化测试技巧 Google大神 ...

  2. Android 自动化测试框架

    Android常用的自动化测试工具框架: Monkey,MonkeyRunner,UIAutomator,Robotium,Appium,Monkey Talk...... 但这些工具框架都是什么呢有 ...

  3. MonkeyRunner原理初步--Android自动化测试学习历程

    章节:自动化基础篇——MonkeyRunner原理初步 主要讲解内容及笔记: 一.理论知识和脚本演示 最佳方式是上官网文档去查看monkeyrunner的介绍,官网上不去,就找了一个本地的androi ...

  4. 【Mac + Appium + Python3.6学习(五)】之常用的Android自动化测试API总结

    Github测试样例地址:https://github.com/appium-boneyard/sample-code/tree/master/sample-code/examples ①定位text ...

  5. [技术博客] Android 自动化测试

    [技术博客] Android 自动化测试 安卓自动化测试工具与平台的搭建 类似于网页端自动化,安卓测试的自动化也主要是针对控件的自动化.其原理就是通过python(其他语言) 的脚本来代替我们手动完成 ...

  6. Android自动化测试--monkey总结

    什么是 Monkey Monkey 是一个 Android 自动化测试小工具.主要用于Android 的压力测试, 主要目的就是为了测试app 是否会Crash. Monkey 特点 顾名思义,Mon ...

  7. 使用 flow.ci 实现 Android 自动化测试与持续集成

    在上篇文章--如何实现 Android 应用的持续部署中,我们使用的是 flow.ci + Github + fir.im 实现 Android 应用的持续部署.对于 Android 开发者,他们可能 ...

  8. Android自动化测试-Robotium(一)简介

    一.Robotium原理 Robotium是一款Android自动化测试框架,主要针对Android平台的应用进行黑盒自动化测试,它提供了模拟各种手势操作(点击.长按.滑动等).查找和断言机制的API ...

  9. 转:Android开发实践:用脚本编译Android工程

    转自: http://ticktick.blog.51cto.com/823160/1365947 一般情况下,我们都是使用Eclipse+ADT插件或者Android studio软件来编译Andr ...

随机推荐

  1. (转)sql通配符

    背景:一次搞清sql查询中的通配符问题. 1 sql通配符 通配符主要以下几种:%._.[].[^] . 在搜索数据库中的数据时,SQL 通配符可以替代一个或多个字符.SQL 通配符必须与 LIKE ...

  2. (转)Dom4J解析

    xml文档: <?xml version="1.0" encoding="UTF-8"?> <书架> <书 出版社="清 ...

  3. 配置LAMP实现WordPress

    环境说明: 在同一台主机上实现LAMP(Linux + Apache + MariaDB + PHP) CentOS 7.3.Apache 2.4.6.MariaDB 5.5.52.PHP 5.4.1 ...

  4. HDFS笔记——技术点汇总

    目录 · 概况 · 原理 · HDFS 架构 · 块 · NameNode · SecondaryNameNode · fsimage与edits合并 · DataNode · 数据读写 · 容错机制 ...

  5. opnet的sink模块学习 分类: opnet 2014-05-18 10:28 161人阅读 评论(0) 收藏

    Sink模块的状态机很简单,只有INIT和DISCARD两个,非强制状态只有DISCARD用于包的销毁.Sink模块的作用就是销毁从输入流接收到的包,并且返回关于包的一系列统计量. Init的入口代码 ...

  6. 双向循环链表(C语言描述)(一)

    双向循环链表是链表的一种,它的每个节点也包含数据域和指针域.为了方便程序维护,可以单独为数据域定义一种数据类型,这里以整型为例: typedef int LinkedListData; 双向循环链表( ...

  7. 假面舞会[NOI2008]

    题目描述 一年一度的假面舞会又开始了,栋栋也兴致勃勃的参加了今年的舞会.今年的面具都是主办方特别定制的.每个参加舞会的人都可以在入场时选择一 个自己喜欢的面具.每个面具都有一个编号,主办方会把此编号告 ...

  8. LVS-负载均衡集群部署

    简介:LVS是一种集群技术,采用IP负载均衡技术和基于内容请求分发技术,调度器具有很好的吞吐量,将请求均衡的转移到不同服务器上执行,且调度器自动屏蔽掉服务器的故障,从而将一组服务器构成一个高性能,高可 ...

  9. web端常见安全漏洞测试结果分析-- appscan

    基于appscan测试结果分析: 一.XSS跨站脚本 指的是攻击者往Web页面里插入恶意html代码,通常是JavaScript编写的恶意代码,当用户浏览该页之时,嵌入其中Web里面的html代码会被 ...

  10. [bzoj 2438][中山市选2011]杀人游戏 概率+tarjan

    考试的时候想了很多,不知道它那个概率究竟是怎么算..没想到能蒙30分.rp爆发hhh 题解转自不知道哪里来的老师发的(代码出自自己). 实际上警察就是两种结果——查到犯人或死亡,而死亡事件一定是包含在 ...