前提
由於公司業務要求,所以自動化測試要達到以下幾點:
- 跨應用的測試
- 測試用例可讀性強
- 測試報告可讀性強
- 對失敗的用例有截圖保存並在報告中體現
基於以上幾點,在對自動化測試框架選型的時候就選擇了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
由於水平有限,還請大神多指點!謝謝!