使用 UI Automator 编写自动化测试

UI Automator 测试框架提供了一组 API,用于构建与用户应用和系统应用交互的界面测试。

现代 UI Automator 测试简介

UI Automator 2.4 引入了一种精简的、对 Kotlin 友好的领域特定语言 (DSL),可简化 Android 界面测试的编写。这个新的 API 接口侧重于基于谓词的元素查找和对应用状态的显式控制。使用它可创建更易于维护且更可靠的自动化测试。

借助 UI Automator,您可以从应用进程外部测试应用。这样一来,您就可以测试应用了精简功能的发布版本。在编写宏基准测试时,UI Automator 也很有用。

现代方法的主要特点包括:

  • 专用的 uiAutomator 测试范围,可实现更简洁、更具表现力的测试代码。
  • 用于查找具有明确谓词的界面元素的方法,例如 onElementonElementsonElementOrNull
  • 针对条件元素的内置等待机制 onElement*(timeoutMs: Long = 10000)
  • 显式应用状态管理,例如 waitForStablewaitForAppToBeVisible
  • 直接与无障碍窗口节点互动,以进行多窗口测试场景。
  • 内置屏幕截图功能和用于视觉测试与调试的 ResultsReporter

设置项目

如需开始使用新版 UI Automator API,请更新项目的 build.gradle.kts 文件以添加最新依赖项

Kotlin

dependencies {
  ...
  androidTestImplementation("androidx.test.uiautomator:uiautomator:2.4.0-alpha05")
}

Groovy

dependencies {
  ...
  androidTestImplementation "androidx.test.uiautomator:uiautomator:2.4.0-alpha05"
}

核心 API 概念

以下部分介绍了现代 UI Automator API 的核心概念。

uiAutomator 测试范围

uiAutomator { ... } 代码块中访问所有新的 UI Automator API。此函数会创建一个 UiAutomatorTestScope,为您的测试操作提供简洁且类型安全的环境。

uiAutomator {
  // All your UI Automator actions go here
  startApp("com.example.targetapp")
  onElement { textAsString() == "Hello, World!" }.click()
}

查找界面元素

使用带有谓词的 UI Automator API 来定位界面元素。借助这些谓词,您可以为文本、选中或聚焦状态以及内容说明等属性定义条件。

  • onElement { predicate }:在默认超时时间内返回与谓词匹配的第一个界面元素。如果该函数未找到匹配的元素,则会抛出异常。

    // Find a button with the text "Submit" and click it
    onElement { textAsString() == "Submit" }.click()
    
    // Find a UI element by its resource ID
    onElement { id == "my_button_id" }.click()
    
    // Allow a permission request
    watchFor(PermissionDialog) {
      clickAllow()
    }
    
  • onElementOrNull { predicate }:与 onElement 类似,但如果在超时时间内未找到匹配的元素,则返回 null。它不会抛出异常。此方法适用于可选元素。

    val optionalButton = onElementOrNull { textAsString() == "Skip" }
    optionalButton?.click() // Click only if the button exists
    
  • onElements { predicate }:等待直到至少有一个界面元素与给定的谓词匹配,然后返回所有匹配的界面元素的列表。

    // Get all items in a list Ui element
    val listItems = onElements { className == "android.widget.TextView" && isClickable }
    listItems.forEach { it.click() }
    

以下是使用 onElement 通话的一些提示:

  • 针对嵌套元素的链式 onElement 调用:您可以链式调用 onElement 来查找其他元素内的元素,遵循父子层次结构。

    // Find a parent Ui element with ID "first", then its child with ID "second",
    // then its grandchild with ID "third", and click it.
    onElement { id == "first" }
      .onElement { id == "second" }
      .onElement { id == "third" }
      .click()
    
  • 通过传递表示毫秒的值,为 onElement* 函数指定超时时间。

    // Find a Ui element with a zero timeout (instant check)
    onElement(0) { id == "something" }.click()
    
    // Find a Ui element with a custom timeout of 10 seconds
    onElement(10_000) { textAsString() == "Long loading text" }.click()
    

与界面元素互动

通过模拟点击或在可修改字段中设置文本来与界面元素互动。

// Click a Ui element
onElement { textAsString() == "Tap Me" }.click()

// Set text in an editable field
onElement { className == "android.widget.EditText" }.setText("My input text")

// Perform a long click
onElement { contentDescription == "Context Menu" }.longClick()

处理应用状态和监听器

管理应用的生命周期,并处理测试期间可能出现的意外界面元素。

应用生命周期管理

这些 API 提供了一些方法来控制受测应用的状态:

// Start a specific app by package name. Used for benchmarking and other
// self-instrumenting tests.
startApp("com.example.targetapp")

// Start a specific activity within the target app
startActivity(SomeActivity::class.java)

// Start an intent
startIntent(myIntent)

// Clear the app's data (resets it to a fresh state)
clearAppData("com.example.targetapp")

处理意外的界面

借助 watchFor API,您可以为测试流程中可能出现的意外界面元素(例如权限对话框)定义处理程序。此方法使用内部监控器机制,但可提供更高的灵活性。

import androidx.test.uiautomator.PermissionDialog

@Test
fun myTestWithPermissionHandling() = uiAutomator {
  startActivity(MainActivity::class.java)

  // Register a watcher to click "Allow" if a permission dialog appears
  watchFor(PermissionDialog) { clickAllow() }

  // Your test steps that might trigger a permission dialog
  onElement { textAsString() == "Request Permissions" }.click()

  // Example: You can register a different watcher later if needed
  clearAppData("com.example.targetapp")

  // Now deny permissions
  startApp("com.example.targetapp")
  watchFor(PermissionDialog) { clickDeny() }
  onElement { textAsString() == "Request Permissions" }.click()
}

PermissionDialogScopedWatcher<T> 的一个示例,其中 T 是作为范围传递给 watchFor 中块的对象。您可以根据此模式创建自定义监听器。

等待应用曝光度和稳定性

有时,测试需要等待元素变得可见或稳定。UI Automator 提供了多个 API 来帮助实现此目的。

waitForAppToBeVisible("com.example.targetapp") 会等待具有指定软件包名称的界面元素在可自定义的超时时间内出现在屏幕上。

// Wait for the app to be visible after launching it
startApp("com.example.targetapp")
waitForAppToBeVisible("com.example.targetapp")

使用 waitForStable() API 验证应用的界面是否被视为稳定,然后再与之互动。

// Wait for the entire active window to become stable
activeWindow().waitForStable()

// Wait for a specific Ui element to become stable (e.g., after a loading animation)
onElement { id == "my_loading_indicator" }.waitForStable()

高级功能

以下功能适用于更复杂的测试场景。

与多个窗口互动

通过 UI Automator API,您可以直接与界面元素进行交互并检查界面元素。这对于涉及多个窗口的场景(例如画中画 [PiP] 模式或分屏布局)特别有用。

// Find the first window that is in Picture-in-Picture mode
val pipWindow = windows()
  .first { it.isInPictureInPictureMode == true }

// Now you can interact with elements within that specific window
pipWindow.onElement { textAsString() == "Play" }.click()

屏幕截图和视觉断言

直接在测试中截取整个屏幕、特定窗口或单个界面元素的屏幕截图。这有助于进行视觉回归测试和调试。

uiautomator {
  // Take a screenshot of the entire active window
  val fullScreenBitmap: Bitmap = activeWindow().takeScreenshot()
  fullScreenBitmap.saveToFile(File("/sdcard/Download/full_screen.png"))

  // Take a screenshot of a specific UI element (e.g., a button)
  val buttonBitmap: Bitmap = onElement { id == "my_button" }.takeScreenshot()
  buttonBitmap.saveToFile(File("/sdcard/Download/my_button_screenshot.png"))

  // Example: Take a screenshot of a PiP window
  val pipWindowScreenshot = windows()
    .first { it.isInPictureInPictureMode == true }
    .takeScreenshot()
  pipWindowScreenshot.saveToFile(File("/sdcard/Download/pip_screenshot.png"))
}

Bitmap 的 saveToFile 扩展函数可简化将捕获的图片保存到指定路径的操作。

使用 ResultsReporter 进行调试

借助 ResultsReporter,您可以将测试制品(例如屏幕截图)直接与 Android Studio 中的测试结果相关联,以便更轻松地进行检查和调试。

uiAutomator {
  startApp("com.example.targetapp")

  val reporter = ResultsReporter("MyTestArtifacts") // Name for this set of results
  val file = reporter.addNewFile(
    filename = "my_screenshot",
    title = "Accessible button image" // Title that appears in Android Studio test results
  )

  // Take a screenshot of an element and save it using the reporter
  onElement { textAsString() == "Accessible button" }
    .takeScreenshot()
    .saveToFile(file)

  // Report the artifacts to instrumentation, making them visible in Android Studio
  reporter.reportToInstrumentation()
}

从旧版 UI Automator 迁移

如果您有使用旧版 API 表面编写的现有 UI Automator 测试,请使用下表作为参考来迁移到新方法:

操作类型 旧版 UI Automator 方法 新的 UI Automator 方法
入口点 UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) 将测试逻辑封装在 uiAutomator { ... } 范围内。
查找界面元素 device.findObject(By.res("com.example.app:id/my_button")) onElement { id == "my\_button" }
查找界面元素 device.findObject(By.text("Click Me")) onElement { textAsString() == "Click Me" }
等待界面空闲 device.waitForIdle() 首选 onElement 的内置超时机制;否则,首选 activeWindow().waitForStable()
查找子元素 手动嵌套的 findObject 调用 onElement().onElement() 链式调用
处理权限对话框 UiAutomator.registerWatcher() watchFor(PermissionDialog)