使用 UI Automator 编写自动化测试

UI Automator 是一个 UI 测试框架,适用于跨系统应用和已安装应用的跨应用功能性 UI 测试。UI Automator API 允许你与设备上可见的元素进行交互,无论哪个 Activity 处于焦点,因此它允许你在测试设备中执行诸如打开“设置”菜单或应用启动器等操作。你的测试可以通过使用方便的描述符(例如该组件中显示的文本或其内容描述)来查找 UI 组件。

UI Automator 测试框架是一个基于仪器化 (instrumentation) 的 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 组件。为确保 UI Automator 测试可以访问这些组件,请检查这些组件是否具有可见文本标签、android:contentDescription 值或两者兼有。

uiautomatorviewer 工具提供了一个方便的可视化界面,用于检查布局层次结构并查看设备前台可见的 UI 组件的属性。此信息可让你使用 UI Automator 创建更精细的测试。例如,你可以创建一个匹配特定可见属性的 UI 选择器。

启动 uiautomatorviewer 工具:

  1. 在物理设备上启动目标应用。
  2. 将设备连接到你的开发机器。
  3. 打开终端窗口并导航到 <android-sdk>/tools/ 目录。
  4. 使用此命令运行工具:
 $ uiautomatorviewer

查看应用的 UI 属性:

  1. uiautomatorviewer 界面中,点击 设备屏幕截图 按钮。
  2. 将鼠标悬停在左侧面板中的快照上,以查看 uiautomatorviewer 工具识别的 UI 组件。属性列在右下角面板中,布局层次结构列在右上角面板中。
  3. (可选)点击 切换 NAF 节点 按钮,查看 UI Automator 无法访问的 UI 组件。这些组件可能只有有限的信息可用。

要了解 Android 提供的常见 UI 组件类型,请参阅用户界面

确保你的 Activity 可访问

UI Automator 测试框架在已实现 Android 无障碍功能的应用上表现更好。当你使用 View 类型或 SDK 中 View 的子类的 UI 元素时,你无需实现无障碍功能支持,因为这些类已经为你完成了。

然而,一些应用使用自定义 UI 元素来提供更丰富的用户体验。这些元素不会提供自动无障碍功能支持。如果你的应用包含非 SDK 的 View 子类的实例,请务必通过完成以下步骤向这些元素添加无障碍功能:

  1. 创建一个扩展 ExploreByTouchHelper 的具体类。
  2. 通过调用 setAccessibilityDelegate(),将你的新类的实例与特定的自定义 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 对象执行设备级操作,例如强制设备进入特定旋转、按下方向键硬件按钮以及按下主屏幕和菜单按钮。

从设备的主屏幕开始你的测试是一个好习惯。从主屏幕(或你在设备中选择的其他起始位置),你可以调用 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 实例。

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 测试框架允许你通过 getContext() 获取 Context 对象,从而发送 Intent 或启动 Activity,而无需使用 shell 命令。

以下代码片段展示了你的测试如何使用 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());

等待过渡

Turn off disturb
图 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 的更多信息,请查阅以下资源。

参考文档

示例