前提

由于公司业务要求,所以自动化测试要达到以下几点:

  • 跨应用的测试
  • 测试用例可读性强
  • 测试报告可读性强
  • 对失败的用例有截图保存并在报告中体现

基于以上几点,在对自动化测试框架选型的时候就选择了uiautomator,这个是谷歌官方推荐的一个界面自动化测试工具,能跨应用测试

对于测试用例的可读性就选择了cucumber-android。可以通过中文来描述用例,并且能够生成html的测试报告。(用过calabash的童鞋会了解这块内容)

准备

软件安装

  • JDK1.8
  • anddoidStudio
  • androidSDK

涉及工具和框架

  • uiautomator
  • cucumber-andorid
  • cucumber-html

用例设计

用一个简单的计算器来作为例子,用例设计包括加减乘除运算

如下是两个简单的用例,是不是很直观。

场景: 验证基本的减功能
当 输入数字30
当 输入运算符-
当 输入数字20
当 输入运算符=
那么 验证运算结果15
场景: 验证基本的加功能
当 输入数字30
当 输入运算符+
当 输入数字25
当 输入运算符=
那么 验证运算结果55

测试代码设计

测试工程创建

  1. 通过androidStudio新建一个Empty Activity工程,工程中的src目录下会包含androidTest,测试用例代码会在这个目录下来编写
  2. 目录结构如下

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自动化测试的更多相关文章

  1. Android Native App自动化测试实战讲解(上)(基于python)

    1.Native App自动化测试及Appuim框架介绍 android平台提供了一个基于java语言的测试框架uiautomator,它一个测试的Java库,包含了创建UI测试的各种API和执行自动 ...

  2. 移动app自动化测试

    原文出处https://www.toutiao.com/i6473606106970063374/ 原文作者是今日头条的:一个字头的诞生 在此感谢原文作者的无私分享! 移动App自动化测试(一) 目前 ...

  3. Android App自动化测试实战(基于Python)(三)

    1.Native App自动化测试及Appuim框架介绍 android平台提供了一个基于java语言的测试框架uiautomator,它一个测试的Java库,包含了创建UI测试的各种API和执行自动 ...

  4. App自动化测试方案

    App自动化测试方案 1.1  概述 什么是App自动化?为什么要做App自动化? App自动化是指给 Android或iOS上的软件应用程序做的自动化测试. 手工测试和自动化测试的对比如下: 手工测 ...

  5. UIautomator2框架快速入门App自动化测试

    01.APP测试框架比较 常见的APP测试框架   APP测试框架 02.UIAutomator2简介 简介 UIAutomator2是一个可以使用Python对Android设备进行UI自动化的库. ...

  6. APP自动化测试中Monkey和 MonkeyRunner

    在设计了测试用例并通过评审之后,由测试人员根据测试用例中描述的规程步步执行测试,得到实际结果与期望结果的比较.在此过程中,为了节省人力.时间或硬件资源,提高测试效率,便引入了自动化测试的概念.自动化测 ...

  7. 黑盒测试在App自动化测试中的应用

    黑盒测试在App自动化测试中的应用 不废话,直接来. 先说说什么是黑盒测试 黑盒测试,这里就说的是app功能测试,之前看到一个介绍说,就是在测试中,把测试对象看作一个黑盒子.利用黑盒测试法进行动态测试 ...

  8. 老李分享:android app自动化测试工具合集

    老李分享:android app自动化测试工具合集   poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开发等工作为目标.如果对课程感兴趣,请大家咨 ...

  9. 篇3 安卓app自动化测试-搞定界面元素

    篇3                 安卓app自动化测试-搞定界面元素 --lamecho辣么丑 1.1概要 大家好! 我是lamecho(辣么丑),今天是<安卓app自动化测试>的第三 ...

随机推荐

  1. 【BZOJ3622】已经没有什么好害怕的了(动态规划+广义容斥)

    点此看题面 大致题意: 有\(n\)个糖果和\(n\)个药片,各有自己的能量.将其两两配对,求糖果比药片能量大的组数恰好比药片比糖果能量大的组数多\(k\)组的方案数. 什么是广义容斥(二项式反演) ...

  2. Poj(2236),简单并查集

    题目链接:http://poj.org/problem?id=2236 思路很简单,傻逼的我输出写成了FALL,然后遍历的时候for循环写错了,还好很快我就Debug出来了. #include < ...

  3. 【[SCOI2015]小凸玩矩阵】

    题目 第\(k\)大显然没有什么办法直接求,于是多一个\(log\)来二分一波 现在的问题变成了判断一个\(mid\)是否能成为第\(k\)大 这还是一个非常经典的棋盘模型,于是经典的做法就是转化成二 ...

  4. Performing User-Managed Database-18.5、Restoring Control Files

    版权声明:本文为博主原创文章.未经博主同意不得转载. https://blog.csdn.net/offbeatmine/article/details/28429339 18.5.Restoring ...

  5. 复杂链表的复制 -python编写

    题目描述 输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head.(注意,输出结果中请不要返回参数中的节点引用,否 ...

  6. Python中的__name__和__main__含义详解

    1背景 在写Python代码和看Python代码时,我们常常可以看到这样的代码: ? 1 2 3 4 5 def main():     ......   if __name == "__m ...

  7. python :编写装饰器

    简单装饰器 def log_time(func): # 此函数的作用时接受被修饰的函数的引用test,然后被内部函数使用 def make_decorater(): print('现在开始装饰') f ...

  8. File类,递归

    File类 File文件和目录路径名的抽象表示形式.即,Java中把文件或者目录(文件夹)都封装成File对象. File类包含     路径    path E:\...     目录 direct ...

  9. django批量form表单处理

    1.应用说明 一般在表单信息录入中,如果存在许多重复提交的信息,我们就需要进行批量处理,比如学生信息的批量录入. 这里一种方式就是使用xlrd模块处理,把学生信息录入到系统内 另外一种方式就是采用我们 ...

  10. 转 MYSQL 命令行大全 (简洁、明了、全面)

    MYSQL常用命令 .导出整个数据库 mysqldump -u 用户名 -p –default-character-set=latin1 数据库名 > 导出的文件名(数据库默认编码是latin1 ...