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
- 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
- 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!