uiautomator+cucumber實現移動app自動化測試


前提

由於公司業務要求,所以自動化測試要達到以下幾點:

  • 跨應用的測試
  • 測試用例可讀性強
  • 測試報告可讀性強
  • 對失敗的用例有截圖保存並在報告中體現

基於以上幾點,在對自動化測試框架選型的時候就選擇了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

由於水平有限,還請大神多指點!謝謝!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM