使用 UI Automator 編寫自動化測試

UI Automator 是 UI 測試架構,適用於系統和已安裝應用程式的跨應用程式功能 UI 測試。UI Automator API 可讓您與裝置上可見的元素互動,無論哪個 Activity 處於焦點,都能夠執行操作,例如在測試裝置上開啟「設定」選單或應用程式啟動器。測試可以使用方便的描述詞彙 (例如該元件中顯示的文字或其內容說明) 來查詢 UI 元件。

UI Automator 測試架構是一種以檢測為基礎的 API,可與 AndroidJUnitRunner 測試執行器搭配使用。這類測試程式碼不會依賴目標應用程式的內部實作細節,因此非常適合用於編寫不透明方塊式自動化測試。

UI Automator 測試架構的主要功能包括:

  • 擷取狀態資訊並在目標裝置上執行作業的 API。詳情請參閱「存取裝置狀態」。
  • 支援跨應用程式 UI 測試的 API。詳情請參閱「UI Automator API」。

存取裝置狀態

UI Automator 測試架構會提供 UiDevice 類別,用於存取目標應用程式執行裝置並執行相關作業。您可以呼叫其方法來存取裝置屬性,例如目前的方向或顯示大小。UiDevice 類別還可讓您執行下列動作:

  1. 變更裝置旋轉。
  2. 按下硬體鍵,例如「調高音量」。
  3. 按下「返回」、「主畫面」或「選單」按鈕。
  4. 開啟通知欄。
  5. 擷取目前視窗的螢幕截圖。

舉例來說,如要模擬按下主畫面按鈕,請呼叫 UiDevice.pressHome() 方法。

UI Automator API

您可以使用 UI Automator API 編寫可靠的測試,而無須瞭解指定應用程式的實作詳細資料。您可以使用下列 API 擷取及操作多個應用程式的 UI 元件:

  • UiObject2:代表在裝置上顯示的 UI 元素。
  • BySelector:指定比對 UI 元素的條件。
  • By:以簡潔的方式建構 BySelector
  • Configurator:可讓您設定執行 UI Automator 測試的關鍵參數。

舉例來說,以下程式碼顯示如何編寫測試指令碼,在裝置上開啟 Gmail 應用程式:

Kotlin

device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
device.pressHome()

val gmail: UiObject2 = device.findObject(By.text("Gmail"))
// Perform a click and wait until the app is opened.
val opened: Boolean = gmail.clickAndWait(Until.newWindow(), 3000)
assertThat(opened).isTrue()

Java

device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
device.pressHome();

UiObject2 gmail = device.findObject(By.text("Gmail"));
// Perform a click and wait until the app is opened.
Boolean opened = gmail.clickAndWait(Until.newWindow(), 3000);
assertTrue(opened);

設定 UI Automator

使用 UI Automator 建構 UI 測試前,請務必依照「設定 AndroidX Test 的專案」一文的說明設定測試原始碼位置和專案依附元件。

在 Android 應用程式模組的 build.gradle 檔案中,您必須設定對 UI Automator 程式庫的依附元件參照:

Kotlin

dependencies {
  ...
  androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
}

Groovy

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

如要最佳化 UI Automator 測試,請先檢查目標應用程式的 UI 元件,並確保可存取這些元件。這些最佳化提示會在接下來兩個部分中說明。

檢查裝置上的使用者介面

設計測試前,請檢查裝置上顯示的 UI 元件。為確保 UI Automator 測試能存取這些元件,請檢查這些元件是否有可見的文字標籤或 android:contentDescription 值,或兩者皆有。

uiautomatorviewer 工具提供方便的視覺化介面,可用於檢查版面配置階層,以及查看裝置前景可見的 UI 元件屬性。這項資訊可讓您使用 UI Automator 建立更精細的測試。例如,您可以建立符合特定可見屬性的 UI 選取器。

如要啟動 uiautomatorviewer 工具,請按照下列步驟操作:

  1. 在實體裝置上啟動目標應用程式。
  2. 將裝置連接至開發機器。
  3. 開啟終端機視窗,然後前往 <android-sdk>/tools/ 目錄。
  4. 請使用下列指令執行工具:
 $ uiautomatorviewer

如要查看應用程式的 UI 屬性,請按照下列步驟操作:

  1. uiautomatorviewer 介面中,按一下「Device Screenshot」按鈕。
  2. 將滑鼠游標懸停在左側面板中的快照上,即可查看 uiautomatorviewer 工具識別的 UI 元件。屬性會列於右下方面板,而版面配置階層則會列於右上方面板。
  3. 您可以按一下「切換 NAF 節點」按鈕,查看 UI Automator 無法存取的 UI 元件。這些元件可能只提供有限的資訊。

如要瞭解 Android 提供的常見 UI 元件類型,請參閱「使用者介面」一文。

確保活動可供存取

在已實作 Android 無障礙功能的應用程式上,UI Automator 測試架構的效能會更好。如果您使用 View 類型的 UI 元素,或 SDK 中的 View 子類別,就不需要實作無障礙支援,因為這些類別已為您完成這項工作。

不過,有些應用程式會使用自訂 UI 元素,提供更豐富的使用者體驗。這類元素不會提供自動無障礙支援。如果您的應用程式包含非 SDK 的 View 子類別例項,請務必完成下列步驟,為這些元素新增無障礙功能:

  1. 建立擴充 ExploreByTouchHelper 的具體類別。
  2. 呼叫 setAccessibilityDelegate(),將新類別的例項與特定自訂 UI 元素建立關聯。

如要進一步瞭解如何在自訂檢視畫面元素中新增無障礙功能,請參閱「建構無障礙自訂檢視畫面」。如要進一步瞭解 Android 無障礙功能的一般最佳做法,請參閱「讓應用程式更易於存取」。

建立 UI Automator 測試類別

您的 UI Automator 測試類別應以與 JUnit 4 測試類別相同的方式編寫。如要進一步瞭解如何建立 JUnit 4 測試類別,以及使用 JUnit 4 斷言和註解,請參閱「建立檢測設備單元測試類別」。

在測試類別定義開頭處加入 @RunWith(AndroidJUnit4.class) 註解。您還需要指定 AndroidX 測試中提供的 AndroidJUnitRunner 類別,做為預設測試執行器。如要進一步瞭解這個步驟,請參閱「在裝置或模擬器上執行 UI Automator 測試」。

在 UI Automator 測試類別中實作下列程式設計模式:

  1. 請呼叫 getInstance() 方法,並將 Instrumentation 物件做為引數,藉此取得 UiDevice 物件,以便存取要測試的裝置。
  2. 呼叫 findObject() 方法,取得 UiObject2 物件,以便存取裝置上顯示的 UI 元件 (例如前景中的目前檢視畫面)。
  3. 呼叫 UiObject2 方法,模擬要在該 UI 元件上執行的特定使用者互動,例如呼叫 scrollUntil() 以捲動,以及呼叫 setText() 以編輯文字欄位。您可以視需要重複呼叫步驟 2 和 3 中的 API,以便測試涉及多個 UI 元件或使用者動作序列的更複雜使用者互動。
  4. 在執行這些使用者互動後,請檢查 UI 是否反映預期的狀態或行為。

下節將詳細說明這些步驟。

存取 UI 元件

UiDevice 物件是您存取及操控裝置狀態的主要方式。在測試中,您可以呼叫 UiDevice 方法,檢查各種屬性的狀態,例如目前的方向或顯示大小。測試可以使用 UiDevice 物件執行裝置層級動作,例如強制裝置進入特定旋轉角度、按下方向鍵硬體按鈕,以及按下主畫面和選單按鈕。

建議您從裝置的主畫面開始測試。您可以在主畫面 (或裝置中選擇的其他起始位置) 呼叫 UI Automator API 提供的方法,選取並與特定 UI 元素互動。

以下程式碼片段顯示測試如何取得 UiDevice 的例項,並模擬按下 Home 鍵:

Kotlin

import org.junit.Before
import androidx.test.runner.AndroidJUnit4
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
...

private const val BASIC_SAMPLE_PACKAGE = "com.example.android.testing.uiautomator.BasicSample"
private const val LAUNCH_TIMEOUT = 5000L
private const val STRING_TO_BE_TYPED = "UiAutomator"

@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = 18)
class ChangeTextBehaviorTest2 {

private lateinit var device: UiDevice

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

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

  // Wait for launcher
  val launcherPackage: String = device.launcherPackageName
  assertThat(launcherPackage, notNullValue())
  device.wait(
    Until.hasObject(By.pkg(launcherPackage).depth(0)),
    LAUNCH_TIMEOUT
  )

  // Launch the app
  val context = ApplicationProvider.getApplicationContext<Context>()
  val intent = context.packageManager.getLaunchIntentForPackage(
  BASIC_SAMPLE_PACKAGE).apply {
    // Clear out any previous instances
    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
    )
  }
}

Java

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) 陳述式可確保測試只在搭載 Android 4.3 (API 級別 18) 以上版本的裝置上執行,符合 UI Automator 架構的要求。

使用 findObject() 方法擷取 UiObject2,代表符合指定選取器條件的檢視畫面。您可以視需要重複使用在應用程式測試其他部分建立的 UiObject2 例項。請注意,每次測試使用 UiObject2 例項點選 UI 元素或查詢屬性時,UI Automator 測試架構都會搜尋目前顯示畫面中的相符項目。

以下程式碼片段說明測試如何建構代表應用程式中「取消」按鈕和「確定」按鈕的 UiObject2 例項。

Kotlin

val okButton: UiObject2 = device.findObject(
    By.text("OK").clazz("android.widget.Button")
)

// Simulate a user-click on the OK button, if found.
if (okButton != null) {
    okButton.click()
}

Java

UiObject2 okButton = device.findObject(
    By.text("OK").clazz("android.widget.Button")
);

// Simulate a user-click on the OK button, if found.
if (okButton != null) {
    okButton.click();
}

指定選取器

如果您想存取應用程式中的特定 UI 元件,請使用 By 類別建構 BySelector 例項。BySelector 代表對顯示 UI 中特定元素的查詢。

如果找到多個相符元素,系統會將版面配置階層中的第一個相符元素傳回做為目標 UiObject2。建構 BySelector 時,您可以將多個資源鏈結在一起,以便精進搜尋結果。如果找不到相符的 UI 元素,系統會傳回 null

您可以使用 hasChild()hasDescendant() 方法巢狀排列多個 BySelector 例項。舉例來說,以下程式碼範例說明測試如何指定搜尋作業,找出第一個含有文字屬性子項 UI 元素的 ListView

Kotlin

val listView: UiObject2 = device.findObject(
    By.clazz("android.widget.ListView")
        .hasChild(
            By.text("Apps")
        )
)

Java

UiObject2 listView = device.findObject(
    By.clazz("android.widget.ListView")
        .hasChild(
            By.text("Apps")
        )
);

在選取器條件中指定物件狀態可能很有幫助。舉例來說,如果您想選取所有已勾選元素的清單,以便取消勾選這些元素,請將引數設為 true 並呼叫 checked() 方法。

執行動作

測試取得 UiObject2 物件後,您就可以呼叫 UiObject2 類別中的各項方法,在該物件所代表的 UI 元件上執行使用者互動。您可以指定以下動作:

  • click():點選 UI 元素可見邊界區塊的中心。
  • drag():將這個物件拖曳至任意座標。
  • setText():清除欄位內容後,在可編輯欄位中設定文字。相反地,clear() 方法會清除可編輯欄位中的現有文字。
  • swipe():執行滑動動作,朝指定方向滑動。
  • scrollUntil():向指定方向執行捲動動作,直到 ConditionEventCondition 滿足為止。

透過 UI 自動化測試架構,您可以透過 getContext() 取得 Context 物件,在不使用殼層指令的情況下,傳送Intent 或啟動Activity

以下程式碼片段說明測試如何使用意圖啟動測試中的應用程式。如果您只想測試計算機應用程式,而不需要啟動器,這種做法就很實用。

Kotlin

fun setUp() {
...

  // Launch a simple calculator app
  val context = getInstrumentation().context
  val intent = context.packageManager.getLaunchIntentForPackage(CALC_PACKAGE).apply {
    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)
}

Java

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

驗證結果

InstrumentationTestCase 會擴充 TestCase,因此您可以使用標準 JUnit Assert 方法,測試應用程式中的 UI 元件是否會傳回預期的結果。

以下程式碼片段說明測試如何在計算機應用程式中找出多個按鈕,依序按下按鈕,然後驗證是否顯示正確的結果。

Kotlin

private const val CALC_PACKAGE = "com.myexample.calc"

fun testTwoPlusThreeEqualsFive() {
  // Enter an equation: 2 + 3 = ?
  device.findObject(By.res(CALC_PACKAGE, "two")).click()
  device.findObject(By.res(CALC_PACKAGE, "plus")).click()
  device.findObject(By.res(CALC_PACKAGE, "three")).click()
  device.findObject(By.res(CALC_PACKAGE, "equals")).click()

  // Verify the result = 5
  val result: UiObject2 = device.findObject(By.res(CALC_PACKAGE, "result"))
  assertEquals("5", result.text)
}

Java

private static final String CALC_PACKAGE = "com.myexample.calc";

public void testTwoPlusThreeEqualsFive() {
  // Enter an equation: 2 + 3 = ?
  device.findObject(By.res(CALC_PACKAGE, "two")).click();
  device.findObject(By.res(CALC_PACKAGE, "plus")).click();
  device.findObject(By.res(CALC_PACKAGE, "three")).click();
  device.findObject(By.res(CALC_PACKAGE, "equals")).click();

  // Verify the result = 5
  UiObject2 result = device.findObject(By.res(CALC_PACKAGE, "result"));
  assertEquals("5", result.getText());
}

在裝置或模擬器上執行 UI Automator 測試

您可以透過 Android Studio 或指令列執行 UI Automator 測試。請務必將 AndroidJUnitRunner 指定為專案中的預設檢測執行器。

其他示例

與系統 UI 互動

UI Automator 可以與畫面上的所有內容互動,包括應用程式以外的系統元素,如以下程式碼片段所示:

Kotlin

// Opens the System Settings.
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
device.executeShellCommand("am start -a android.settings.SETTINGS")

Java

// Opens the System Settings.
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
device.executeShellCommand("am start -a android.settings.SETTINGS");

Kotlin

// Opens the notification shade.
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
device.openNotification()

Java

// Opens the notification shade.
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
device.openNotification();

Kotlin

// Opens the Quick Settings shade.
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
device.openQuickSettings()

Java

// Opens the Quick Settings shade.
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
device.openQuickSettings();

Kotlin

// Get the system clock.
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
UiObject2 clock = device.findObject(By.res("com.android.systemui:id/clock"))
print(clock.getText())

Java

// Get the system clock.
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
UiObject2 clock = device.findObject(By.res("com.android.systemui:id/clock"));
print(clock.getText());

等待轉換

關閉零打擾模式
圖 1. UI Automator 會在測試裝置上關閉「零打擾」模式。

螢幕轉場可能需要一些時間,而且無法可靠地預測其持續時間,因此您應讓 UI Automator 在執行作業後等待。UI Automator 提供多種方法來執行這項操作:

下列程式碼片段說明如何使用 UI Automator,透過等待轉換的 performActionAndWait() 方法,在系統設定中關閉「請勿打擾」模式:

Kotlin

@Test
@SdkSuppress(minSdkVersion = 21)
@Throws(Exception::class)
fun turnOffDoNotDisturb() {
    device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
    device.performActionAndWait({
        try {
            device.executeShellCommand("am start -a android.settings.SETTINGS")
        } catch (e: IOException) {
            throw RuntimeException(e)
        }
    }, Until.newWindow(), 1000)
    // Check system settings has been opened.
    Assert.assertTrue(device.hasObject(By.pkg("com.android.settings")))

    // Scroll the settings to the top and find Notifications button
    var scrollableObj: UiObject2 = device.findObject(By.scrollable(true))
    scrollableObj.scrollUntil(Direction.UP, Until.scrollFinished(Direction.UP))
    val notificationsButton = scrollableObj.findObject(By.text("Notifications"))

    // Click the Notifications button and wait until a new window is opened.
    device.performActionAndWait({ notificationsButton.click() }, Until.newWindow(), 1000)
    scrollableObj = device.findObject(By.scrollable(true))
    // Scroll down until it finds a Do Not Disturb button.
    val doNotDisturb = scrollableObj.scrollUntil(
        Direction.DOWN,
        Until.findObject(By.textContains("Do Not Disturb"))
    )
    device.performActionAndWait({ doNotDisturb.click() }, Until.newWindow(), 1000)
    // Turn off the Do Not Disturb.
    val turnOnDoNotDisturb = device.findObject(By.text("Turn on now"))
    turnOnDoNotDisturb?.click()
    Assert.assertTrue(device.wait(Until.hasObject(By.text("Turn off now")), 1000))
}

Java

@Test
@SdkSuppress(minSdkVersion = 21)
public void turnOffDoNotDisturb() throws Exception{
    device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
    device.performActionAndWait(() -> {
        try {
            device.executeShellCommand("am start -a android.settings.SETTINGS");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }, Until.newWindow(), 1000);
    // Check system settings has been opened.
    assertTrue(device.hasObject(By.pkg("com.android.settings")));

    // Scroll the settings to the top and find Notifications button
    UiObject2 scrollableObj = device.findObject(By.scrollable(true));
    scrollableObj.scrollUntil(Direction.UP, Until.scrollFinished(Direction.UP));
    UiObject2 notificationsButton = scrollableObj.findObject(By.text("Notifications"));

    // Click the Notifications button and wait until a new window is opened.
    device.performActionAndWait(() -> notificationsButton.click(), Until.newWindow(), 1000);
    scrollableObj = device.findObject(By.scrollable(true));
    // Scroll down until it finds a Do Not Disturb button.
    UiObject2 doNotDisturb = scrollableObj.scrollUntil(Direction.DOWN,
            Until.findObject(By.textContains("Do Not Disturb")));
    device.performActionAndWait(()-> doNotDisturb.click(), Until.newWindow(), 1000);
    // Turn off the Do Not Disturb.
    UiObject2 turnOnDoNotDisturb = device.findObject(By.text("Turn on now"));
    if(turnOnDoNotDisturb != null) {
        turnOnDoNotDisturb.click();
    }
    assertTrue(device.wait(Until.hasObject(By.text("Turn off now")), 1000));
}

其他資源

如要進一步瞭解如何在 Android 測試中使用 UI Automator,請參閱下列資源。

參考說明文件:

範例