UI Automator 相關(guān)介紹:
  • 跨應(yīng)用的用戶界面自動(dòng)化測(cè)試
  • 包含在 AndroidX Test(https://developer.android.com/training/testing) 中
  • 支持的 Android 系統(tǒng):>= Android 4.3 (API level 18)
  • 基于 instrumentation,依賴于 AndroidJUnitRunner 測(cè)試運(yùn)行器

設(shè)置 UI Automator(Set up UI Automator)

在編寫(xiě)測(cè)試代碼前,先確保以下兩個(gè)配置:
1、測(cè)試代碼存放位置
2、項(xiàng)目依賴(https://developer.android.com/training/testing/set-up-project)

(1) 添加 Gradle 依賴(Add Gradle dependencies)

  • app 目錄下的 build.gradle 添加:
allprojects {
    repositories {
        jcenter()
        google()
    }
}
  • dependencies 添加需要的 AndroidX Test Package, 比如:
dependencies {
    ...
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
}
  • 如果測(cè)試代碼需要基于 junit 的類(lèi),比如 Assert 和 TestSuiteLoader,在 android 區(qū)塊中添加(只需要添加需要用到的 library:https://developer.android.com/training/testing/set-up-project#junit-based-libs):
android {
    ...

    // Gradle automatically adds 'android.test.runner' as a dependency.
    useLibrary 'android.test.runner'

    useLibrary 'android.test.base'
    useLibrary 'android.test.mock'
}

(2) 添加 manifest 聲明(Add manifest declarations)
此步驟可選,具體請(qǐng)看 https://developer.android.com/training/testing/set-up-project#add-manifest-declarations

當(dāng)前面的配置完成后,進(jìn)行其他配置:
app下的build.gralde:

dependencies {
    ...
    androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
}

當(dāng)所有配置都完成后,進(jìn)行被測(cè)應(yīng)用的 UI 組件分析,確保能被識(shí)別以及接入控制。

檢查設(shè)備上的用戶界面(Inspect the UI on a device)

uiautomatorviewer:

(1) 啟動(dòng)手機(jī)上的被測(cè)應(yīng)用

(2) 手機(jī)連接電腦

(3) 打開(kāi) Terminal, 進(jìn)入目錄 /tools/

(4) 運(yùn)行:uiautomatorviewer

查看應(yīng)用的用戶界面屬性:

(1) 點(diǎn)擊左上角 "Device Screenshot" 按鈕

(2) 左邊是 UI 組件,右下半部分是屬性,右上半部分是布局層級(jí)

(3) 可選功能:點(diǎn)擊右上角 "Toggle NAF Nodes" 按鈕(黃色三角形,內(nèi)有感嘆號(hào)),查看無(wú)法被識(shí)別/訪問(wèn)的UI組件。---這個(gè)功能我都沒(méi)搞懂怎么用,點(diǎn)擊后貌似沒(méi)效果

確保 activity 可訪問(wèn)(Ensure your activity is accessible)

Android 原生元素具有更好的訪問(wèn)性,利于測(cè)試代碼的編寫(xiě),無(wú)需額外的支持
如果是自定義 UI 元素,需要(1)創(chuàng)建一個(gè)繼承自 ExploreByTouchHelper 的實(shí)體類(lèi)(2)通過(guò)調(diào)用 setAccessibilityDelegate() 將新創(chuàng)建的類(lèi)的實(shí)例和特定的自定義 UI 元素相關(guān)聯(lián)
給自定義視圖元素添加無(wú)障礙功能的其他參考資料:https://developer.android.com/guide/topics/ui/accessibility/custom-views.html
學(xué)習(xí)資料 for 提高 Android 的無(wú)障礙性/可訪問(wèn)性:https://developer.android.com/guide/topics/ui/accessibility/apps.html

創(chuàng)建一個(gè) UI Automator 測(cè)試類(lèi)(Create a UI Automator test class)

UI Automator 測(cè)試類(lèi)的寫(xiě)法和 JUnit 4 測(cè)試類(lèi)的寫(xiě)法是一樣的。
JUnit 4 測(cè)試類(lèi)的學(xué)習(xí)資料:https://developer.android.com/training/testing/unit-testing/instrumented-unit-tests.html#build

在測(cè)試類(lèi)開(kāi)頭添加注解:@RunWith(AndroidJUnit4.class)
同時(shí),明確 AndroidX Test 中的 AndroidJUnitRunner 類(lèi)為默認(rèn)的測(cè)試運(yùn)行器。這個(gè)步驟的詳細(xì)描述:https://developer.android.com/training/testing/ui-testing/uiautomator-testing.html#run

在 UI Automator 測(cè)試類(lèi)中執(zhí)行以下編程模型:

  1. 獲取一個(gè) UiDevice 對(duì)象去接入測(cè)試設(shè)備,調(diào)用 getInstance() 方法,傳入 Instrumentation 對(duì)象作為參數(shù)。
  2. 通過(guò) UiObject 對(duì)象調(diào)用 findObject() 方法接入顯示在設(shè)備上的 UI 組件(例如,當(dāng)前手機(jī)屏幕顯示的用戶界面)。
  3. 通過(guò)調(diào)用 UiObject 方法在 UI 組件上模擬一個(gè)交互的動(dòng)作。例如,調(diào)用 performMultiPointerGesture() 方法模擬多指觸控,調(diào)用 setText() 方法編輯文本框。當(dāng)測(cè)試包含多個(gè) UI 組件或者更加復(fù)雜的操作序列時(shí),在第二步和第三步中可重復(fù)調(diào)用各種 API.
  4. 當(dāng)執(zhí)行完這些用戶交互的動(dòng)作后,檢查返回的結(jié)果是否符合預(yù)期。
    這些步驟在以下章節(jié)會(huì)講的更加詳細(xì)。

訪問(wèn)用戶界面組件 (Access UI components)

UiDevice: 接入和控制設(shè)備狀態(tài)的首要方法,可執(zhí)行設(shè)備級(jí)別的行為,例如改變屏幕旋轉(zhuǎn)方向、按下硬件按鈕、以及點(diǎn)擊 home 和 menu 鍵。

從設(shè)備的主屏幕開(kāi)始測(cè)試是一個(gè)好的實(shí)踐。在主屏幕(或者其他你在設(shè)備上選定的開(kāi)始位置),可以調(diào)用 UI Automator API 提供的方法和指定的 UI 元素進(jìn)行交互。

以下代碼片段展示了如何獲取一個(gè) UiDevice 的實(shí)例以及模擬按下 home 鍵的操作:

import org.junit.Before;
import androidx.test.runner.AndroidJUnit4;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.Until;
...
@RunWith(AndroidJUnit4.class)
@SdkSuppress(minSdkVersion = 18)
public class ChangeTextBehaviorTest {

    private static final String BASIC_SAMPLE_PACKAGE
            = "com.example.android.testing.uiautomator.BasicSample";
    private static final int LAUNCH_TIMEOUT = 5000;
    private static final String STRING_TO_BE_TYPED = "UiAutomator";
    private UiDevice device;

    @Before
    public void startMainActivityFromHomeScreen() {
        // Initialize UiDevice instance
        device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());

        // Start from the home screen
        device.pressHome();

        // Wait for launcher
        final String launcherPackage = device.getLauncherPackageName();
        assertThat(launcherPackage, notNullValue());
        device.wait(Until.hasObject(By.pkg(launcherPackage).depth(0)),
                LAUNCH_TIMEOUT);

        // Launch the app
        Context context = ApplicationProvider.getApplicationContext();
        final Intent intent = context.getPackageManager()
                .getLaunchIntentForPackage(BASIC_SAMPLE_PACKAGE);
        // Clear out any previous instances
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
        context.startActivity(intent);

        // Wait for the app to appear
        device.wait(Until.hasObject(By.pkg(BASIC_SAMPLE_PACKAGE).depth(0)),
                LAUNCH_TIMEOUT);
    }
}

示例代碼中的聲明:@SdkSuppress(minSdkVersion = 18), 幫助確定測(cè)試只運(yùn)行在 Android4.3(API level 18)或更高級(jí)別的設(shè)備上。(UI Automator 框架要求的)

  • findObject()
  • UiObject
UiObject cancelButton = device.findObject(new UiSelector()
        .text("Cancel")
        .className("android.widget.Button"));
UiObject okButton = device.findObject(new UiSelector()
        .text("OK")
        .className("android.widget.Button"));

// Simulate a user-click on the OK button, if found.
if(okButton.exists() && okButton.isEnabled()) {
    okButton.click();
}

指定一個(gè)選擇器(Specify a selector)

UiSelector 類(lèi):在當(dāng)前顯示的用戶界面中查詢一個(gè)特定的元素。

  • childSelector()
  • UiAutomatorObjectNotFoundException
UiObject appItem = device.findObject(new UiSelector()
        .className("android.widget.ListView")
        .instance(0)
        .childSelector(new UiSelector()
        .text("Apps")));

tips:

  • 如果在一個(gè)頁(yè)面上找到一個(gè)以上的相同元素,自動(dòng)返回第一個(gè)匹配的元素作為目標(biāo) UiObject.
  • 可以通過(guò)整合多個(gè)屬性來(lái)縮小搜索范圍。
  • 如果沒(méi)有找到目標(biāo)元素,拋出 UiAutomatorObjectNotFoundException 異常。
  • 可以使用 childSelector() 方法縮小多個(gè) UiSelector 實(shí)例范圍。
  • 如果有 Resource ID, 用這個(gè)代替 text 和 content-descripter.
  • text 元素比較脆弱,有多種原因可能導(dǎo)致測(cè)試失敗。(比如:多語(yǔ)言)
    在選擇區(qū)域中去明確一個(gè)對(duì)象狀態(tài)是非常有用的。比如:選擇一個(gè)已選中的列表以進(jìn)行取消選中狀態(tài),調(diào)用 checked() 方法,將參數(shù)設(shè)為 "true".

執(zhí)行動(dòng)作(Perform actions)

當(dāng)獲取 UiObject 對(duì)象后,可以調(diào)用 UiObject 類(lèi)中的方法在其上執(zhí)行相應(yīng)操作:

  • click(): 點(diǎn)擊
  • dragTo(): 拖動(dòng)
  • setText(): 設(shè)置文本
  • clearTextField(): 清空文本
  • swipeUp(): 向上滑動(dòng)
  • swipeDown(): 向下滑動(dòng)
  • swipeLeft(): 向左滑動(dòng)
  • swipeRight(): 向右滑動(dòng)

通過(guò) getContext() 方法獲取到 Context 后,可以進(jìn)行發(fā)送 Intent 或者啟動(dòng) Activity 的操作。

public void setUp() {
    ...

    // Launch a simple calculator app
    Context context = getInstrumentation().getContext();
    Intent intent = context.getPackageManager()
            .getLaunchIntentForPackage(CALC_PACKAGE);
    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);

    // Clear out any previous instances
    context.startActivity(intent);
    device.wait(Until.hasObject(By.pkg(CALC_PACKAGE).depth(0)), TIMEOUT);
}

在集合上執(zhí)行動(dòng)作(Perform actions on collections)

  • UiCollection 類(lèi):在一個(gè) item 的集合上模擬用戶操作(例如,歌曲列表或者郵件列表)。
    如何創(chuàng)建一個(gè) UiCollection 對(duì)象:明確一個(gè)搜索UI容器或者其他子 UI 元素集合的 UiSelector. 例如,包含子 UI 元素的 layout 視圖。
UiCollection videos = new UiCollection(new UiSelector()
        .className("android.widget.FrameLayout"));

// Retrieve the number of videos in this collection:
int count = videos.getChildCount(new UiSelector()
        .className("android.widget.LinearLayout"));

// Find a specific video and simulate a user-click on it
UiObject video = videos.getChildByText(new UiSelector()
        .className("android.widget.LinearLayout"), "Cute Baby Laughing");
video.click();

// Simulate selecting a checkbox that is associated with the video
UiObject checkBox = video.getChild(new UiSelector()
        .className("android.widget.Checkbox"));
if(!checkBox.isSelected()) checkbox.click();

在可滾動(dòng)視圖中執(zhí)行動(dòng)作(Perform actions on scrollable views)

  • UiScrollable 類(lèi):在手機(jī)屏幕上模擬垂直或者水平的滾動(dòng)操作。這個(gè)類(lèi)適用于當(dāng)需要找到屏幕外的 UI 元素時(shí),可以通過(guò)滾動(dòng)操作將這個(gè) UI 元素帶到屏幕內(nèi)。
UiScrollable settingsItem = new UiScrollable(new UiSelector()
        .className("android.widget.ListView"));
UiObject about = settingsItem.getChildByText(new UiSelector()
        .className("android.widget.LinearLayout"), "About tablet");
about.click();

驗(yàn)證結(jié)果(Verify results)

InstrumentationTestCase 繼承自 TestCase,可以使用標(biāo)準(zhǔn)的 JUnit Assert 方法進(jìn)行結(jié)果驗(yàn)證。
以下代碼片段展示了如何驗(yàn)證計(jì)算器加法:

private static final String CALC_PACKAGE = "com.myexample.calc";
public void testTwoPlusThreeEqualsFive() {
    // Enter an equation: 2 + 3 = ?
    device.findObject(new UiSelector()
            .packageName(CALC_PACKAGE).resourceId("two")).click();
    device.findObject(new UiSelector()
            .packageName(CALC_PACKAGE).resourceId("plus")).click();
    device.findObject(new UiSelector()
            .packageName(CALC_PACKAGE).resourceId("three")).click();
    device.findObject(new UiSelector()
            .packageName(CALC_PACKAGE).resourceId("equals")).click();
    // Verify the result = 5
    UiObject result = device.findObject(By.res(CALC_PACKAGE, "result"));
    assertEquals("5", result.getText());
}

在設(shè)備或虛擬機(jī)上運(yùn)行 UI Automator 測(cè)試用例(Run UI Automator tests on a device or emulator)

可以通過(guò) Android Studio 或者命令行運(yùn)行 UI Automator tests. 確保項(xiàng)目的默認(rèn) instrumentation runner 是 AndroidJUnitRunner.