uiautomator+cucumber实现移动app自动化测试
前提
由于公司业务要求,所以自动化测试要达到以下几点:
- 跨应用的测试
- 测试用例可读性强
- 测试报告可读性强
- 对失败的用例有截图保存并在报告中体现
基于以上几点,在对自动化测试框架选型的时候就选择了uiautomator,这个是谷歌官方推荐的一个界面自动化测试工具,能跨应用测试
对于测试用例的可读性就选择了cucumber-android。可以通过中文来描述用例,并且能够生成html的测试报告。(用过calabash的童鞋会了解这块内容)
准备
软件安装
- JDK1.8
- anddoidStudio
- androidSDK
涉及工具和框架
- uiautomator
- cucumber-andorid
- cucumber-html
用例设计
用一个简单的计算器来作为例子,用例设计包括加减乘除运算
如下是两个简单的用例,是不是很直观。
场景: 验证基本的减功能
当 输入数字30
当 输入运算符-
当 输入数字20
当 输入运算符=
那么 验证运算结果15
场景: 验证基本的加功能
当 输入数字30
当 输入运算符+
当 输入数字25
当 输入运算符=
那么 验证运算结果55
测试代码设计
测试工程创建
- 通过androidStudio新建一个Empty Activity工程,工程中的src目录下会包含androidTest,测试用例代码会在这个目录下来编写
- 目录结构如下
assets/features: 放置的是测试用例文件(中文描述的用例文件)
com.cucumber.demo.test: 目录下放置的是测试代码
elements: 界面上的元素获取方法类(后期UI属性发生变化,可修改这个包下面的类即可)
hooks: 放置测试执行的钩子(用例前处理,后处理操作)
runner: 测试用例执行类
steps: 封装的测试步骤脚本
工程配置
由于采用的是cucumber-android框架,并且报告的格式期望是html格式,所以在app/build.gradle中要引入这两个相关依赖。
androidTestCompile 'info.cukes:cucumber-android:1.2.5'
androidTestCompile 'info.cukes:cucumber-picocontainer:1.2.5'
androidTestCompile 'info.cukes:cucumber-html:0.2.3'
androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
在app/build.gradle所有的配置
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "25.0.2"
dexOptions {
incremental true
javaMaxHeapSize "4g"
}
defaultConfig {
applicationId "com.cucumber.demo"
minSdkVersion 18
targetSdkVersion 23
versionCode 1
versionName "1.0"
jackOptions {
enabled true
}
testApplicationId "com.cucumber.demo.test"
testInstrumentationRunner "com.cucumber.demo.test.runner.Instrumentation"
}
packagingOptions {
exclude 'LICENSE.txt'
exclude 'META-INF/maven/com.google.guava/guava/pom.properties'
exclude 'META-INF/maven/com.google.guava/guava/pom.xml'
}
sourceSets {
androidTest {
assets.srcDirs = ['src/androidTest/assets']
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.1.1'
androidTestCompile 'com.android.support.test:runner:0.5'
androidTestCompile 'info.cukes:cucumber-android:1.2.5'
androidTestCompile 'info.cukes:cucumber-picocontainer:1.2.5'
androidTestCompile 'info.cukes:cucumber-html:0.2.3'
androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
androidTestCompile 'com.android.support.test:rules:0.5'
}
如果在编译的时候出现OutOfMemoryError,就在gradle.properties文件中加入下面配置
gradle.properties
org.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=4096m -XX:+HeapDumpOnOutOfMemoryError
测试脚本编写
为了便于维护,将元素获取功能放在一个单独的类中,后期界面有变化的话,可以维护这一份文件即可。
elements/CalculatorActivity.java
package com.cucumber.demo.test.elements;
import android.support.test.InstrumentationRegistry;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject;
import android.support.test.uiautomator.UiObjectNotFoundException;
import android.support.test.uiautomator.UiSelector;
/**
* Created by ogq on 4/19/17.
*/
public class CalculatorActivity {
private static final UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
/**
* 获取数字按键
* @param num
* @return
*/
public static UiObject getNumBtn(String num){
return uiDevice.findObject(new UiSelector().resourceId("com.android.calculator2:id/digit" + num));
}
/**
* 获取运算符和非数字字符
* @param op
* @return
* @throws UiObjectNotFoundException
*/
public static UiObject getCharBtn(String op) throws UiObjectNotFoundException {
switch (op) {
case "+":
return uiDevice.findObject(new UiSelector().resourceId("com.android.calculator2:id/plus"));
case "-":
return uiDevice.findObject(new UiSelector().resourceId("com.android.calculator2:id/minus"));
case "x":
return uiDevice.findObject(new UiSelector().resourceId("com.android.calculator2:id/mul"));
case "/":
return uiDevice.findObject(new UiSelector().resourceId("com.android.calculator2:id/div"));
case "%":
return uiDevice.findObject(new UiSelector().resourceId("com.android.calculator2:id/pct"));
case "=":
return uiDevice.findObject(new UiSelector().resourceId("com.android.calculator2:id/equal"));
case ".":
return uiDevice.findObject(new UiSelector().resourceId("com.android.calculator2:id/dot"));
default:
throw new UiObjectNotFoundException("运算符不正确");
}
}
/**
* 获取清除按钮
* @return
*/
public static UiObject getClsBtn(){
return uiDevice.findObject(new UiSelector().resourceId("com.android.calculator2:id/clear"));
}
/**
* 获取计算结果
* @return
*/
public static UiObject getResultView(){
return uiDevice.findObject(new UiSelector().className("android.widget.EditText"));
}
}
用例都是由步骤来组成,所以步骤实现放在一个类中,进行元素的操作动作。
在类开始指定用例文件路径和胶水代码路径,格式为html
steps/AppTestSteps.java
package com.cucumber.demo.test.steps;
import android.support.test.uiautomator.UiObject;
import android.support.test.uiautomator.UiObjectNotFoundException;
import android.test.ActivityInstrumentationTestCase2;
import android.util.Log;
import com.cucumber.demo.MainActivity;
import com.cucumber.demo.test.elements.CalculatorActivity;
import com.cucumber.demo.test.runner.SomeDependency;
import cucumber.api.CucumberOptions;
import cucumber.api.java.zh_cn.假如;
import cucumber.api.java.zh_cn.那么;
/**
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
*/
@CucumberOptions(features="features", glue = "com.cucumber.demo.test", format={"pretty","html:/data/data/com.cucumber.demo/reports"})
public class AppTestStep extends ActivityInstrumentationTestCase2<MainActivity>{
final String TAG = "AUTOTEST";
public AppTestStep(SomeDependency dependency) {
super(MainActivity.class);
assertNotNull(dependency);
}
@假如("^输入数字(\\S+)$")
public void input_number(String number) throws UiObjectNotFoundException {
Log.v(TAG, "输入数字为:" + number);
char[] chars = number.toCharArray();
for(int i = 0; i < chars.length; i++){
if (chars[i] == '.'){
CalculatorActivity.getCharBtn(String.valueOf(chars[i])).click();
}
else {
CalculatorActivity.getNumBtn(String.valueOf(chars[i])).click();
}
}
}
@假如("^输入运算符([+-x\\/=])$")
public void input_op(String op) throws UiObjectNotFoundException {
Log.v(TAG, "输入运算符为:" + op);
CalculatorActivity.getCharBtn(op).click();
}
@假如("^计算器归零$")
public void reset_calc() throws UiObjectNotFoundException {
Log.v(TAG, "计算器归零");
UiObject clear_obj = CalculatorActivity.getClsBtn();
if (clear_obj.waitForExists(3000)){
clear_obj.click();
}
}
@那么("^验证运算结果(\\S+)$")
public void chk_result(String result) throws UiObjectNotFoundException {
Log.v(TAG, "期望运算结果为:" + result);
UiObject result_obj = CalculatorActivity.getResultView();
if (result_obj.waitForExists(5000)){
String act_result = result_obj.getText();
Log.v(TAG, "实际运算结果为:" + act_result);
if (!result.equals(act_result)) {
throw new UiObjectNotFoundException("结果比对异常,期望值是:" + result + ",实际值是:" + act_result);
}
}else{
throw new UiObjectNotFoundException("结果控件不存在");
}
}
}
执行用例时会涉及到一些环境初始化或者数据清理的操作,此时需要用到用例前处理和后处理,在cucumber-android框架中用hooks来实现这块的功能,Before和After钩子是针对每个用例的前处理和后处理操作。
在截图时,考虑到权限问题,我把图片默认放在测试用例的应用目录下,由于要把图片嵌入到报告中,需要先把图片转为byte[]格式,在由cucumber-android读入,cucumber-android会重新生成一个图片,所以在截图的时候只需要一个固定的名称即可,防止失败用例过多,图片文件会占用很大空间。
前处理: 判断当前是否计算器界面,如果不是的话打开计算器应用,如果是就计算器归零操作。
后处理:判断用例状态,如果用例失败,截图并把截图嵌入到测试报告中。
hooks/TestHooks.java
package com.cucumber.demo.test.hooks;
import android.support.test.InstrumentationRegistry;
import android.support.test.uiautomator.By;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject;
import android.support.test.uiautomator.UiObject2;
import android.support.test.uiautomator.UiObjectNotFoundException;
import android.support.test.uiautomator.UiSelector;
import android.util.Log;
import com.cucumber.demo.test.elements.CalculatorActivity;
import java.util.List;
import cucumber.api.Scenario;
import cucumber.api.java.Before;
import cucumber.api.java.After;
import cucumber.api.Scenario.*;
/**
* Created by ogq on 4/18/17.
*/
public class TestHooks {
final UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
final String TAG = "AUTOTEST-HOOKS";
@Before
public void befor_features() throws UiObjectNotFoundException {
//判断当前是否打开被测应用
String curPkgName = uiDevice.getCurrentPackageName();
Log.v(TAG,"当前的包名为");
Log.v(TAG, curPkgName);
if (curPkgName.equals("com.android.calculator2")){
// 计算器归零
CalculatorActivity.getClsBtn().click();
return;
}
// 打开应用
uiDevice.pressHome();
List<UiObject2> bottom_btns = uiDevice.findObjects(By.clazz("android.widget.TextView"));
for (int i =0;i < bottom_btns.size();i++){
if (i==2){
((UiObject2)bottom_btns.toArray()[i]).click();
}
}
UiObject calc = uiDevice.findObject(new UiSelector().text("Calculator").packageName("com.android.launcher"));
if (calc.waitForExists(3000)){
calc.clickAndWaitForNewWindow();
}else{
throw new UiObjectNotFoundException("计算器应用没有找到");
}
}
@After
public void after_features(Scenario scenario){
Log.v(TAG,"当前的用例名称:" + scenario.getName());
Log.v(TAG,"当前的用例状态:" + scenario.getStatus());
if (status.equals("passed")){
return;
}
String cur_path = "/data/data/com.cucumber.demo";
// String png_name = (new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date())) + ".png";
String png_name = "error.png";
String png_path = cur_path + '/' + png_name;
uiDevice.takeScreenshot(new File(png_path));
byte[] imageAsByte = HelpTools.image2Bytes(png_path);
scenario.embed(imageAsByte, "image/png");
Log.v(TAG, "用例《" + name + "》失败截图成功!");
}
}
重新定义用例执行器,采用的是cucumber-android框架,所以要采用cucumber的执行方式。
runner/Instrumentation.java
package com.cucumber.demo.test.runner;
import android.os.Bundle;
import android.support.test.runner.MonitoringInstrumentation;
import cucumber.api.android.CucumberInstrumentationCore;
public class Instrumentation extends MonitoringInstrumentation {
private final CucumberInstrumentationCore instrumentationCore = new CucumberInstrumentationCore(this);
@Override
public void onCreate(final Bundle bundle) {
super.onCreate(bundle);
instrumentationCore.create(bundle);
start();
}
@Override
public void onStart() {
waitForIdleSync();
instrumentationCore.start();
}
}
runner/SomeDependency.java
package com.cucumber.demo.test.runner;
// Dummy class to demonstrate dependency injection
public class SomeDependency {
}
此时需要修改build.gradle文件,指定测试执行类。
testApplicationId "com.cucumber.demo.test"
testInstrumentationRunner "com.cucumber.demo.test.runner.Instrumentation"
测试用例编写
测试框架采用的是cucumber-android,用例的语法采用的是Gherkin,如果不了解的同学可以网上搜索一下相关内容,还是很容易搜索到的。个人觉得还是值得学习的。
用例文件的编写采用中文描述(下面分别用两种方式编写的用例,场景和场景大纲模式)
其中,场景大纲适合操作相同,输入输出不同的场景。
# language: zh-CN
功能: 验证计算器的加减乘除功能
场景大纲: 验证基本的加减乘除功能
当 输入数字<num>
当 输入运算符<op>
当 输入数字<num1>
当 输入运算符<op1>
那么 验证运算结果<result>
例子:
| num | op | num1 | op1 | result |
| 20 | + | 10 | = | 30 |
| 30 | - | 15 | = | 15 |
| 30 | x | 5 | = | 150 |
| 30 | / | 5 | = | 5 |
features/calcute_demo_01.feature
# language: zh-CN
功能: 验证计算器的加减乘除功能
场景: 验证基本的减功能
当 输入数字30
当 输入运算符-
当 输入数字20
当 输入运算符=
那么 验证运算结果15
场景: 验证基本的加功能
当 输入数字30
当 输入运算符+
当 输入数字25
当 输入运算符=
那么 验证运算结果55
运行用例
通过androidStudio的build和assembleAndroidTest任务会在app/build/output/apk目录下生成app-debug.apk和app-debug-androidTest-unaligned.apk
安装apk
adb install -r app-debug.apk
adb install -r app-debug-androidTest-unaligned.apk
验证安装
adb shell pm list instrumentation
查看测试用例信息(最下面的一条)

运行用例
adb shell am instrument -w -r com.cucumber.demo.test/.runner.Instrumentation
报告查看
因为故意在用例中写了个失败的用例场景,所以在结果中会有失败的场景。
HTML报告
在步骤类中指定的/data/data/com.cucumber.demo/reports/目录下也会有相应的html报告,可以通过以下命令下载下来查看报告:
adb pull /data/data/com.cucumber.demo/reports/ ./
通过浏览器打开reports/index.html

文本报告
INSTRUMENTATION_STATUS: numtests=4
INSTRUMENTATION_STATUS: test=场景大纲 验证基本的加减乘除功能
INSTRUMENTATION_STATUS: class=功能 验证计算器的加减乘除功能
INSTRUMENTATION_STATUS_CODE: 1
INSTRUMENTATION_STATUS: numtests=4
INSTRUMENTATION_STATUS: test=场景大纲 验证基本的加减乘除功能
INSTRUMENTATION_STATUS: class=功能 验证计算器的加减乘除功能
INSTRUMENTATION_STATUS_CODE: 0
INSTRUMENTATION_STATUS: numtests=4
INSTRUMENTATION_STATUS: test=场景大纲 验证基本的加减乘除功能
INSTRUMENTATION_STATUS: class=功能 验证计算器的加减乘除功能
INSTRUMENTATION_STATUS_CODE: 1
INSTRUMENTATION_STATUS: numtests=4
INSTRUMENTATION_STATUS: test=场景大纲 验证基本的加减乘除功能
INSTRUMENTATION_STATUS: class=功能 验证计算器的加减乘除功能
INSTRUMENTATION_STATUS_CODE: 0
INSTRUMENTATION_STATUS: numtests=4
INSTRUMENTATION_STATUS: test=场景大纲 验证基本的加减乘除功能
INSTRUMENTATION_STATUS: class=功能 验证计算器的加减乘除功能
INSTRUMENTATION_STATUS_CODE: 1
INSTRUMENTATION_STATUS: numtests=4
INSTRUMENTATION_STATUS: test=场景大纲 验证基本的加减乘除功能
INSTRUMENTATION_STATUS: class=功能 验证计算器的加减乘除功能
INSTRUMENTATION_STATUS_CODE: 0
INSTRUMENTATION_STATUS: numtests=4
INSTRUMENTATION_STATUS: test=场景大纲 验证基本的加减乘除功能
INSTRUMENTATION_STATUS: class=功能 验证计算器的加减乘除功能
INSTRUMENTATION_STATUS_CODE: 1
INSTRUMENTATION_STATUS: numtests=4
INSTRUMENTATION_STATUS: test=场景大纲 验证基本的加减乘除功能
INSTRUMENTATION_STATUS: class=功能 验证计算器的加减乘除功能
INSTRUMENTATION_STATUS: stack=android.support.test.uiautomator.UiObjectNotFoundException: 结果比对异常,期望值是:5,实际值是:6
at com.cucumber.demo.test.steps.AppTestStep.chk_result(AppTestStep.java:73)
at ✽.那么验证运算结果5(features/calcute_demo.feature:13)
INSTRUMENTATION_STATUS_CODE: -1
INSTRUMENTATION_CODE: -1
演示
demo演示视频地址:http://v.youku.com/v_show/id_XMjcyNjA2MTExNg==.html
后期扩展
- 能够让对代码了解不多的测试人员,也可以参与到自动化测试用例的编写中来
- 搭建一个服务器,把测试脚本上传到该服务器,提供界面,让测试人员上传编写好的用例文件,触发编译构建,生成测试用例APK,然后可以下载下来安装并测试,也是比较方便的。
源码地址
源码git地址:https://github.com/ouguangqian/uiautomator-cucumber-demo
由于水平有限,还请大神多指点!谢谢!
uiautomator+cucumber实现移动app自动化测试的更多相关文章
- Android Native App自动化测试实战讲解(上)(基于python)
1.Native App自动化测试及Appuim框架介绍 android平台提供了一个基于java语言的测试框架uiautomator,它一个测试的Java库,包含了创建UI测试的各种API和执行自动 ...
- 移动app自动化测试
原文出处https://www.toutiao.com/i6473606106970063374/ 原文作者是今日头条的:一个字头的诞生 在此感谢原文作者的无私分享! 移动App自动化测试(一) 目前 ...
- Android App自动化测试实战(基于Python)(三)
1.Native App自动化测试及Appuim框架介绍 android平台提供了一个基于java语言的测试框架uiautomator,它一个测试的Java库,包含了创建UI测试的各种API和执行自动 ...
- App自动化测试方案
App自动化测试方案 1.1 概述 什么是App自动化?为什么要做App自动化? App自动化是指给 Android或iOS上的软件应用程序做的自动化测试. 手工测试和自动化测试的对比如下: 手工测 ...
- UIautomator2框架快速入门App自动化测试
01.APP测试框架比较 常见的APP测试框架 APP测试框架 02.UIAutomator2简介 简介 UIAutomator2是一个可以使用Python对Android设备进行UI自动化的库. ...
- APP自动化测试中Monkey和 MonkeyRunner
在设计了测试用例并通过评审之后,由测试人员根据测试用例中描述的规程步步执行测试,得到实际结果与期望结果的比较.在此过程中,为了节省人力.时间或硬件资源,提高测试效率,便引入了自动化测试的概念.自动化测 ...
- 黑盒测试在App自动化测试中的应用
黑盒测试在App自动化测试中的应用 不废话,直接来. 先说说什么是黑盒测试 黑盒测试,这里就说的是app功能测试,之前看到一个介绍说,就是在测试中,把测试对象看作一个黑盒子.利用黑盒测试法进行动态测试 ...
- 老李分享:android app自动化测试工具合集
老李分享:android app自动化测试工具合集 poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开发等工作为目标.如果对课程感兴趣,请大家咨 ...
- 篇3 安卓app自动化测试-搞定界面元素
篇3 安卓app自动化测试-搞定界面元素 --lamecho辣么丑 1.1概要 大家好! 我是lamecho(辣么丑),今天是<安卓app自动化测试>的第三 ...
随机推荐
- QT OpenGL中文教程在QT4版本后的错误代码更改(一)
由于教程中说的已经够可以了,这里就不对代码进行分析了,有兴趣可以自己去看看.这个教程来源于原来的NeHeOpenGL中文教程 (http://www.yakergong.net/nehe/) ,但其有 ...
- 【JavaScript 封装库】BETA 1.0 测试版发布!
/* 源码作者: 石不易(Louis Shi) 联系方式: http://www.shibuyi.net =============================================== ...
- lambda Expression的使用方法
Expression<Func<your class, bool>> whereExp = f => true;//类似1=1,初始化条件 if (!string.IsN ...
- POJ-1509 Glass Beads---最小表示法模板
题目链接: https://vjudge.net/problem/POJ-1509 题目大意: 给你一个循环串,然后找到一个位置,使得从这个位置开始的整个串字典序最小. 解题思路: 最小表示法模板 注 ...
- DFS+BFS(POJ3083)
题目链接:http://poj.org/problem?id=3083 解题报告:这个题目,搜最短路,没有什么问题.优先走左边,走右边,有很多说法,思路大概都相同,都是记录当前朝向,根据数学公式(i+ ...
- 1.6 NBU Catalog备份还原
用户的数据保存到了磁盘或者磁带中,并且是安全的,NBU所在的机器还有可能发生故障,需要重新安装或者将NBU部署到其他的机器中继续使用. 在这种情况下,如何让NBU知道用户已经存在的备份策略和存储单元配 ...
- UIView设置阴影无效的原因之一
本想在底部的按钮设置个阴影, 代码如下: self.layer.shadowColor = [UIColor blackColor].CGColor; self.layer.shadowOffset ...
- 2017.10.9 JVM入门学习
1.什么是JVM JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现 ...
- IIS/IIS Express中遇到的证书问题
上面这幅图大家应该不陌生(觉得陌生的话就不用看下面的内容了,呵呵),再放上中英两段关键字: 根据验证过程,远程证书无效. The remote certificate is invalid accor ...
- 用servlet设计OA管理系统时遇到问题
如果不加单引号会使得除变量和int类型的值不能传递 转发和重定向的区别 转发需要填写完整路径,重定向只需要写相对路径.原因是重定向是一次请求之内已经定位到了服务器端,转发则需要两次请求每次都需要完整的 ...
