使用 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")
}

Groovy

dependencies {
  ...
  androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.3.0'
}

如需优化 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)或更高版本的设备上运行(根据 Android 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 实例。例如,以下代码示例展示了您的测试如何指定搜索,以查找第一个具有文本属性的子界面元素的 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 测试框架,您可以发送 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 方法来测试应用中的界面组件是否会返回预期结果。

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

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 通过等待转换的 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,请参阅以下资源。

参考文档:

示例