使用 UI Automator 編寫自動化測試

UI Automator 是一個 UI 測試架構,適合跨系統和已安裝應用程式的跨應用程式功能 UI 測試。無論使用者聚焦的是哪個 Activity,UI Automator API 都可以透過 UI Automator API 與裝置上的可見元素互動,讓您可以在測試裝置中執行「設定」選單或應用程式啟動器等作業。測試可以使用便利的描述元 (例如該元件中顯示的文字或其內容說明) 來查詢 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-alpha03')
}

Groovy

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

如要最佳化 UI Automator 測試,請先檢查目標應用程式的 UI 元件,確認元件可供存取。接下來的兩節將說明這些最佳化提示。

在裝置上檢查 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. 您也可以按一下「Toggle NAF Nodes」(切換 NAF 節點) 按鈕,查看 UI Automator 無法存取的 UI 元件。這些元件僅提供有限的資訊。

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

確認活動的無障礙位置

UI Automator 測試架構在實作 Android 無障礙功能的應用程式中表現較佳。使用 View 類型的 UI 元素,或 SDK 中的 View 子類別時,您不需要實作無障礙功能支援,因為這些類別已經替您完成。

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

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

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

建立 UI Automator 測試類別

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

請在測試類別定義的開頭加入 @RunWith(AndroidJUnit4.class) 註解。您也需要指定 AndroidX Test 中提供的 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 物件執行裝置層級的操作,例如強制裝置進行特定旋轉作業、按下 D-Pad 硬體按鈕,以及按下「主畫面」和「選單」按鈕。

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

下列程式碼片段顯示測試如何取得 UiDevice 的執行個體,並模擬按下主畫面按鈕的情形:

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 執行個體,這些執行個體代表應用程式中的「Cancel」按鈕和「OK」按鈕。

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

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

執行動作

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

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

UI Automator 測試架構可讓您在不使用殼層指令的情況下傳送 Intent 或啟動活動,方法是透過 getContext() 取得 Context 物件。

下列程式碼片段說明測試如何使用 Intent 啟動受測的應用程式。如果您只想測試計算機應用程式,也不關心啟動器,這個方法就非常實用。

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() 方法,在系統設定中關閉「Do Not Disturb」模式:

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,請參閱下列資源。

參考說明文件:

範例