使用 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-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的 UI 元素(或 SDK 中View的子类)时,您无需实现辅助功能支持,因为这些类已经为您完成了这项工作。

但是,有些应用程序使用自定义 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对象来执行设备级操作,例如强制设备进入特定旋转、按下 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实例。

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 测试框架允许您通过 Context 对象(通过 getContext() 获取)发送 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 指定为默认的 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 的更多信息,请参阅以下资源。

参考文档

示例