使用 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. 截取当前窗口的屏幕截图。

例如,要模拟 Home 按钮按下,请调用 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 界面中,单击设备屏幕截图按钮。
  2. 将鼠标悬停在左侧面板中的快照上,以查看 uiautomatorviewer 工具识别的 UI 组件。属性列在右下角面板中,布局层次结构列在右上角面板中。
  3. 可选地,单击切换 NAF 节点按钮以查看 UI Automator 无法访问的 UI 组件。这些组件可能仅提供有限的信息。

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

确保您的活动可访问

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 断言和注释的更多信息,请参阅 创建 Instrumentation 单元测试类

在测试类定义的开头添加 @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 硬件按钮以及按下 Home 和 Menu 按钮。

最好从设备的主屏幕开始您的测试。从主屏幕(或您在设备中选择的其他起始位置),您可以调用 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 Automator 测试框架允许您发送Intent 或启动Activity,无需使用 shell 命令,只需通过 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 指定为默认的 Instrumentation Runner。

更多示例

与系统 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 的更多信息,请参阅以下资源。

参考文档

示例