使用 UI Automator 编写自动化测试

UI Automator 是一个界面测试框架,适用于跨系统和已安装的应用进行跨应用功能界面测试。借助 UI Automator API,您可以与设备上的可见元素进行交互(无论焦点是哪个 Activity),从而能够在测试设备中执行打开“设置”菜单或应用启动器等操作。测试可以使用方便的描述符(例如,显示在相应组件中的文本或其内容说明)来查找界面组件。

UI Automator 测试框架是基于插桩的 API,可与 AndroidJUnitRunner 测试运行程序配合使用。它非常适合编写不透明的盒式自动化测试,在此类测试中,测试代码不依赖于目标应用的内部实现细节。

UI Automator 测试框架的主要功能包括:

  • 用于在目标设备上检索状态信息并执行操作的 API。如需了解详情,请参阅访问设备状态
  • 支持跨应用界面测试的 API。如需了解详情,请参阅 UI Automator API

访问设备状态

UI Automator 测试框架提供了一个 UiDevice 类,用于在运行目标应用的设备上访问和执行操作。您可以调用其方法来访问设备属性,例如当前屏幕方向或显示屏尺寸。借助 UiDevice 类,您还可以执行以下操作:

  1. 改变设备的旋转。
  2. 按硬件键,如“音量调高”。
  3. 按返回、主屏幕或菜单按钮。
  4. 打开通知栏。
  5. 截取当前窗口的屏幕截图。

例如,如需模拟按下主屏幕按钮的操作,请调用 UiDevice.pressHome() 方法。

UI Automator API

借助 UI Automator API,您可以编写稳健的测试,而无需了解目标应用的实现细节。您可以使用这些 API 在多个应用中捕获和操纵界面组件:

  • UiObject2:表示设备上可见的界面元素。
  • BySelector:指定匹配界面元素的条件。
  • 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 构建界面测试之前,请务必配置测试源代码位置和项目依赖项,如针对 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 Automator 测试可以访问这些组件,请检查这些组件是否具有可见文本标签和/或 android:contentDescription 值。

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

要启动 uiautomatorviewer 工具,请执行以下操作:

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

如需查看应用的界面属性,请执行以下操作:

  1. uiautomatorviewer 界面中,点击 Device Screenshot 按钮。
  2. 将鼠标悬停在左侧面板中的快照上,以查看 uiautomatorviewer 工具识别的界面组件。右下方的面板中列出了属性,右上角面板中列出了布局层次结构。
  3. (可选)点击 Toggle NAF Nodes 按钮,以查看 UI Automator 无法访问的界面组件。对于这些组件,系统可能仅提供有限的信息。

如需了解 Android 提供的常见界面组件类型,请参阅界面

确保 Activity 可访问

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

不过,有些应用会使用自定义界面元素来提供更丰富的用户体验。此类元素不会提供自动无障碍功能支持。如果您的应用包含并非来自 SDK 的 View 的子类的实例,请务必向这些元素添加无障碍功能,具体操作步骤如下:

  1. 创建一个扩展 ExploreByTouchHelper 的具体类。
  2. 通过调用 setAccessibilityDelegate(),将新类的实例与特定自定义界面元素相关联。

如需获得有关向自定义视图元素添加无障碍功能的其他指导,请参阅构建无障碍自定义视图。如需详细了解 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 对象,以访问设备上显示的界面组件(例如,前台的当前视图)。
  3. 通过调用 UiObject2 方法,模拟要对该界面组件执行的特定用户互动;例如,调用 scrollUntil() 以滚动页面,调用 setText() 以修改文本字段。 您可以根据需要重复调用第 2 步和第 3 步中的 API,以测试涉及多个界面组件或用户操作序列的更复杂的用户互动。
  4. 执行这些用户互动后,检查界面是否反映了预期的状态或行为。

下面几部分更详细地介绍了这些步骤。

访问界面组件

UiDevice 对象是您访问和操纵设备状态的主要方式。在测试中,您可以调用 UiDevice 方法来检查各种属性的状态,例如当前屏幕方向或显示屏尺寸。您的测试可以使用 UiDevice 对象执行设备级操作,例如强制设备进入特定旋转状态、按方向键硬件按钮,以及按主屏幕和菜单按钮。

最好从设备的主屏幕开始测试。在主屏幕(或您在设备中选择的其他某个起始位置)上,您可以调用 UI Automator API 提供的方法,以选择特定的界面元素并与之交互。

以下代码段展示了您的测试如何获取 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 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();
}

指定选择器

如果您要访问应用中的特定界面组件,请使用 By 类构造 BySelector 实例。BySelector 表示对所显示的界面中特定元素的查询。

如果找到了多个匹配元素,系统会将布局层次结构中的第一个匹配元素作为目标 UiObject2 返回。构建 BySelector 时,您可以将多个属性链接在一起,以优化搜索。如果未找到匹配的界面元素,则返回 null

您可以使用 hasChild()hasDescendant() 方法嵌套多个 BySelector 实例。例如,以下代码示例展示了如何指定搜索来查找第一个具有具有 text 属性的子界面元素的 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 类中的方法,在由该对象表示的界面组件上执行用户互动。您可以指定如下操作:

  • click():点击界面元素的可见边界的中心。
  • drag():将此对象拖动到任意坐标。
  • setText():清除可修改字段的内容后,设置该字段中的文本。相反,clear() 方法会清除可修改字段中的现有文本。
  • swipe():向指定方向执行滑动操作。
  • scrollUntil():向指定方向执行滚动操作,直到满足 ConditionEventCondition

借助 UI Automator 测试框架,您可以通过 getContext() 获取 Context 对象,从而在不使用 shell 命令的情况下发送 Intent 或启动 Activity

以下代码段展示了您的测试如何使用 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 方法来测试应用中的界面组件是否会返回预期结果。

以下代码段展示了您的测试如何找到计算器应用中的多个按钮,按顺序点击各个按钮,然后验证是否显示了正确的结果。

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 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,请参阅以下资源。

参考文档:

示例