uiautomator+cucumber to realize automatic test of mobile app

Posted by ashley526 on Thu, 03 Mar 2022 16:33:40 +0100

premise

Due to the business requirements of the company, the automatic test should meet the following points:

  • Cross application testing
  • Strong readability of test cases
  • The test report is readable
  • Screenshots of failed use cases are saved and reflected in the report

Based on the above points, we chose uiautomator when selecting the automation testing framework. This is an interface automation testing tool officially recommended by Google, which can test across applications
For the readability of test cases, we choose cucumber Android. Use cases can be described in Chinese, and html test reports can be generated. (children's shoes with calabash will understand this content)

prepare

Software installation

  • JDK1.8
  • anddoidStudio
  • androidSDK

Tools and frameworks involved

  • uiautomator
  • cucumber-andorid
  • cucumber-html

Use case design

Using a simple calculator as an example, use case design includes addition, subtraction, multiplication and division
The following are two simple use cases, which are not very intuitive.

scene: Verify the basic subtraction function
          When entering the number 30
          When entering an operator-
          When entering the number 20
          When entering an operator=
          Then verify the operation result 15
 scene: Verify the basic plus function
          When entering the number 30
          When entering an operator+
          When entering the number 25
          When operator input=
          Then verify the operation result 55

Test code design

Test project creation

  1. Create an Empty Activity project through Android studio. The src directory in the project will contain Android test, and the test case code will be written in this directory
  2. The directory structure is as follows

assets/features: test case files (case files described in Chinese) are placed
com.cucumber.demo.test: the test code is placed in the directory
elements: the element acquisition method class on the interface (if the UI attribute changes later, you can modify the class under this package)
Hooks: place hooks for test execution (case pre-processing and post-processing operations)
runner: test case execution class
steps: encapsulated test step script

Engineering configuration

Since the cucumber Android framework is adopted, and the format of the report is expected to be html format, it is in APP / build These two related dependencies will be introduced into 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'

In APP / build All configurations of 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'


    }

If OutOfMemoryError occurs during compilation, it is in gradle Add the following configuration to the properties file
gradle.properties

    org.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=4096m -XX:+HeapDumpOnOutOfMemoryError

Test script writing

In order to facilitate maintenance, put the element acquisition function in a separate class. If the interface changes later, you can maintain this file.
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());


        /**

         * Get numeric keys

         * @param num

         * @return

         */

        public static UiObject getNumBtn(String num){

            return uiDevice.findObject(new UiSelector().resourceId("com.android.calculator2:id/digit" + num));

        }


        /**

         * Get operators and non numeric characters

         * @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("Incorrect operator");

            }

        }


        /**

         * Get clear button

         * @return

         */

        public static UiObject getClsBtn(){

            return uiDevice.findObject(new UiSelector().resourceId("com.android.calculator2:id/clear"));

        }


        /**

         * Get calculation results

         * @return

         */

        public static UiObject getResultView(){

            return uiDevice.findObject(new UiSelector().className("android.widget.EditText"));

        }

    }

Use cases are composed of steps, so the step implementation is placed in a class to operate elements.
Specify the use case file path and glue code path at the beginning of the class. The format is 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.If;

    import cucumber.api.java.zh_cn.that;


    /**

     * <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);

        }



        @If("^Enter number(\\S+)$")

        public void input_number(String number) throws UiObjectNotFoundException {

            Log.v(TAG, "The input number is:" + 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();

                }

            }

        }


        @If("^Input operator([+-x\\/=])$")

        public void input_op(String op) throws UiObjectNotFoundException {

            Log.v(TAG, "The input operator is:" + op);

            CalculatorActivity.getCharBtn(op).click();

        }


        @If("^Zero calculator $")

        public void reset_calc() throws UiObjectNotFoundException {

            Log.v(TAG, "Zero calculator");

            UiObject clear_obj = CalculatorActivity.getClsBtn();

            if (clear_obj.waitForExists(3000)){

                clear_obj.click();

            }

        }


        @that("^Verify operation results(\\S+)$")

        public void chk_result(String result) throws UiObjectNotFoundException {

            Log.v(TAG, "The expected calculation result is:" + result);

            UiObject result_obj = CalculatorActivity.getResultView();

            if (result_obj.waitForExists(5000)){

                String act_result = result_obj.getText();

                Log.v(TAG, "The actual calculation result is:" + act_result);

               if (!result.equals(act_result)) {

                   throw new UiObjectNotFoundException("The result comparison is abnormal, and the expected value is:" + result + ",The actual value is:" +   act_result);

               }

            }else{

                throw new UiObjectNotFoundException("The result control does not exist");

            }

        }

    }

Some environment initialization or data cleaning operations will be involved in the execution of use cases. At this time, use case pre-processing and post-processing are required. Hooks are used to realize this function in the cucumber Android framework. Before and After hooks are pre-processing and post-processing operations for each use case.
In the screenshot, considering the permission problem, I put the picture in the application directory of the test case by default. To embed the picture into the report, you need to convert the picture into byte [] format first. When it is read in by cucumber Android, cucumber Android will regenerate a picture. Therefore, only a fixed name is needed in the screenshot to prevent too many failed cases, Picture files take up a lot of space.

Pre processing: judge whether the calculator interface is currently. If not, open the calculator application. If yes, reset the calculator.
Post processing: judge the use case status. If the use case fails, take the screenshot and embed it into the test report.

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 {

            //Judge whether the application under test is currently opened

            String curPkgName = uiDevice.getCurrentPackageName();

            Log.v(TAG,"The current package name is");

            Log.v(TAG, curPkgName);

            if (curPkgName.equals("com.android.calculator2")){

                // Zero calculator

                CalculatorActivity.getClsBtn().click();

                return;

            }

            //        Open app

            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("Calculator app not found");

            }


        }

        @After

        public void after_features(Scenario scenario){


            Log.v(TAG,"Current use case name:" + scenario.getName());

            Log.v(TAG,"Current use case status:" + 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, "Use case<" + name + ">Failed screenshot successful!");

        }

    }

Redefine the use case executor and adopt the cucumber Android framework, so the implementation mode of cucumber should be adopted.

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 {

    }

At this point, you need to modify build Gradle file, specifying the test execution class.

    testApplicationId "com.cucumber.demo.test"

    testInstrumentationRunner "com.cucumber.demo.test.runner.Instrumentation"

Test case writing

The test framework adopts cucumber Android and the syntax of use cases adopts Gherkin. If you don't know about it, you can search the relevant content online, which is still easy to find. Personally, I think it's worth learning.

Use case files are written in Chinese (use cases, scenarios and scenario outline patterns written in two ways below)
Among them, the scene outline is suitable for scenes with the same operation and different input and output.

    # language: zh-CN

    function: Verify the addition, subtraction, multiplication and division function of the calculator

        Scenario outline: Verify the basic addition, subtraction, multiplication and division function
          When entering a number<num>
          When entering an operator<op>
          When entering a number<num1>
          When entering an operator<op1>
          Then verify the operation result<result>

          example:
            | 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
    function: Verify the addition, subtraction, multiplication and division function of the calculator

        scene: Verify the basic subtraction function
          When entering the number 30
          When entering an operator-
          When entering the number 20
          When entering an operator=
          Then verify the operation result 15
        scene: Verify the basic plus function
          When entering the number 30
          When entering an operator+
          When entering the number 25
          When entering an operator=
          Then verify the operation result 55

Run case

Through the build and assemblyandroidtest tasks of Android studio, APP debug will be generated in the app/build/output/apk directory Apk and app debug Android test unaligned apk

Install apk
    adb install -r app-debug.apk
    adb install -r app-debug-androidTest-unaligned.apk
Verify installation
    adb shell pm list instrumentation 

View test case information (the bottom one)

Run case
adb shell am instrument -w -r com.cucumber.demo.test/.runner.Instrumentation

Report view

Because a failed use case scenario is deliberately written in the use case, there will be a failed scenario in the result.

HTML report

/ data / data / com. Specified in the step class cucumber. There will also be corresponding html reports in the demo / reports / directory. You can download and view the reports through the following commands:

adb pull /data/data/com.cucumber.demo/reports/ ./

Open reports / index. From your browser html

Text report
    INSTRUMENTATION_STATUS: numtests=4

    INSTRUMENTATION_STATUS: test=Scene outline verification basic addition, subtraction, multiplication and division functions

    INSTRUMENTATION_STATUS: class=Addition, subtraction, multiplication and division function of function verification calculator

    INSTRUMENTATION_STATUS_CODE: 1

    INSTRUMENTATION_STATUS: numtests=4

    INSTRUMENTATION_STATUS: test=Scene outline verification basic addition, subtraction, multiplication and division functions

    INSTRUMENTATION_STATUS: class=Addition, subtraction, multiplication and division function of function verification calculator

    INSTRUMENTATION_STATUS_CODE: 0

    INSTRUMENTATION_STATUS: numtests=4

    INSTRUMENTATION_STATUS: test=Scene outline verification basic addition, subtraction, multiplication and division functions

    INSTRUMENTATION_STATUS: class=Addition, subtraction, multiplication and division function of function verification calculator

    INSTRUMENTATION_STATUS_CODE: 1

    INSTRUMENTATION_STATUS: numtests=4

    INSTRUMENTATION_STATUS: test=Scene outline verification basic addition, subtraction, multiplication and division functions

    INSTRUMENTATION_STATUS: class=Addition, subtraction, multiplication and division function of function verification calculator

    INSTRUMENTATION_STATUS_CODE: 0

    INSTRUMENTATION_STATUS: numtests=4

    INSTRUMENTATION_STATUS: test=Scene outline verification basic addition, subtraction, multiplication and division functions

    INSTRUMENTATION_STATUS: class=Addition, subtraction, multiplication and division function of function verification calculator

    INSTRUMENTATION_STATUS_CODE: 1

    INSTRUMENTATION_STATUS: numtests=4

    INSTRUMENTATION_STATUS: test=Scene outline verification basic addition, subtraction, multiplication and division functions

    INSTRUMENTATION_STATUS: class=Addition, subtraction, multiplication and division function of function verification calculator

    INSTRUMENTATION_STATUS_CODE: 0

    INSTRUMENTATION_STATUS: numtests=4

    INSTRUMENTATION_STATUS: test=Scene outline verification basic addition, subtraction, multiplication and division functions

    INSTRUMENTATION_STATUS: class=Addition, subtraction, multiplication and division function of function verification calculator

    INSTRUMENTATION_STATUS_CODE: 1

    INSTRUMENTATION_STATUS: numtests=4

    INSTRUMENTATION_STATUS: test=Scene outline verification basic addition, subtraction, multiplication and division functions

    INSTRUMENTATION_STATUS: class=Addition, subtraction, multiplication and division function of function verification calculator

    INSTRUMENTATION_STATUS: stack=android.support.test.uiautomator.UiObjectNotFoundException: The result comparison is abnormal, and the expected value is:5,The actual value is:6

        at com.cucumber.demo.test.steps.AppTestStep.chk_result(AppTestStep.java:73)

        at ✽.Then verify the operation result 5(features/calcute_demo.feature:13)


    INSTRUMENTATION_STATUS_CODE: -1

    INSTRUMENTATION_CODE: -1

demonstration

Demo demo video address: http://v.youku.com/v_show/id_XMjcyNjA2MTExNg==.html

Late expansion

  • Testers who don't know much about the code can also participate in the writing of automated test cases
  • Build a server, upload the test script to the server, and provide an interface for testers to upload the written case file, trigger the compilation and construction, generate the test case APK, and then download, install and test it. It is also more convenient.

Source address

Source code git address: https://github.com/ouguangqian/uiautomator-cucumber-demo

Due to the limited level, please give more advice! thank you!

Topics: Java Android Android Studio Testing