测试多个应用的界面

通过涉及多个应用中的用户交互的界面测试,您可以验证当用户流跨入其他应用或系统界面时,您的应用是否能够正常运行。短信应用就是此类用户流的一个例子,该应用先让用户输入短信,再启动 Android 联系人选择器,以便用户可以选择短信的收件人,然后将控制权返还给原来的应用,以便用户提交短信。

本课介绍如何使用 AndroidX Test 提供的 UI Automator 测试框架来编写此类界面测试。通过 UI Automator API,您可以与设备上的可见元素进行交互,而不管焦点在哪个 Activity 上。您的测试可以使用方便的描述符(如显示在相应组件中的文本或其内容描述)来查找界面组件。UI Automator 测试可以在搭载 Android 4.3(API 级别 18)或更高版本的设备上运行。

UI Automator 测试框架是基于插桩的 API,可与 AndroidJUnitRunner 测试运行程序一起使用。

您还应阅读 UI Automator API 参考文档,并尝试 UI Automator 代码示例

设置 UI Automator

在使用 UI Automator 构建界面测试之前,请务必配置测试源代码位置和项目依赖项,如针对 AndroidX Test 设置项目中所述。

在 Android 应用模块的 build.gradle 文件中,您必须设置对 UI Automator 库的依赖项引用:

    dependencies {
        ...
        androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.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() 方法,获取 UiObject 对象以访问设备上显示的界面组件(例如,前台的当前视图)。
  3. 通过调用 UiObject 方法,模拟需要在该界面组件上执行的特定用户交互;例如,调用 performMultiPointerGesture() 以模拟多点触控手势,以及调用 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() 方法检索 UiObject,它表示符合给定选择器条件的视图。您可以根据需要重复使用已在应用测试的其他部分中创建的 UiObject 实例。请注意,每当您的测试使用 UiObject 实例以点击界面元素或查询属性时,UI Automator 测试框架都会在当前显示内容中搜索匹配项。

以下代码段展示了您的测试如何构建表示应用中的“取消”按钮和“确定”按钮的 UiObject 实例。

Kotlin

    val cancelButton: UiObject = device.findObject(
            UiSelector().text("Cancel").className("android.widget.Button")
    )
    val okButton: UiObject = device.findObject(
            UiSelector().text("OK").className("android.widget.Button")
    )

    // Simulate a user-click on the OK button, if found.
    if (okButton.exists() && okButton.isEnabled) {
        okButton.click()
    }
    

Java

    UiObject cancelButton = device.findObject(new UiSelector()
            .text("Cancel")
            .className("android.widget.Button"));
    UiObject okButton = device.findObject(new UiSelector()
            .text("OK")
            .className("android.widget.Button"));

    // Simulate a user-click on the OK button, if found.
    if(okButton.exists() && okButton.isEnabled()) {
        okButton.click();
    }
    

指定选择器

如果您需要访问应用中的特定界面组件,请使用 UiSelector 类。此类表示对当前显示的界面中特定元素的查询。

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

您可以使用 childSelector() 方法来嵌套多个 UiSelector 个实例。例如,以下代码示例展示了您的测试如何指定搜索,以在当前显示的界面中查找第一个 ListView,然后在该 ListView 中搜索,以查找具有文本属性“Apps”的界面元素。

Kotlin

    val appItem: UiObject = device.findObject(
            UiSelector().className("android.widget.ListView")
                    .instance(0)
                    .childSelector(
                            UiSelector().text("Apps")
                    )
    )
    

Java

    UiObject appItem = device.findObject(new UiSelector()
            .className("android.widget.ListView")
            .instance(0)
            .childSelector(new UiSelector()
            .text("Apps")));
    

最佳做法是,在指定选择器时,应使用资源 ID(如果已将其分配给界面元素),而不是文本元素或内容描述符。并非所有元素都有文本元素(例如,工具栏中的图标)。文本选择器很脆弱,如果界面发生细微更改,可能会导致测试失败。此外,文本选择器也可能无法在不同语言之间扩展,它们可能与翻译的字符串不匹配。

在选择器条件中指定对象状态可能很有用。例如,如果要选择所有已选中元素的列表以便取消选中这些元素,请调用 checked() 方法并将参数设置为 true

执行操作

您的测试获取 UiObject 对象后,您可以调用 UiObject 类中的方法,在由该对象表示的界面组件上执行用户交互。您可以指定如下操作:

通过 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);
    }
    

对集合执行操作

如果需要模拟内容集合(例如,音乐专辑中的歌曲或收件箱中的电子邮件列表)上的用户交互,请使用 UiCollection 类。要创建 UiCollection 对象,请指定 UiSelector,用于搜索其他子界面元素的界面容器或封装容器,如包含子界面元素的布局视图。

以下代码段展示了您的测试如何构建 UiCollection 以表示 FrameLayout 中显示的视频专辑:

Kotlin

    val videos = UiCollection(UiSelector().className("android.widget.FrameLayout"))

    // Retrieve the number of videos in this collection:
    val count = videos.getChildCount(
            UiSelector().className("android.widget.LinearLayout")
    )

    // Find a specific video and simulate a user-click on it
    val video: UiObject = videos.getChildByText(
            UiSelector().className("android.widget.LinearLayout"),
            "Cute Baby Laughing"
    )
    video.click()

    // Simulate selecting a checkbox that is associated with the video
    val checkBox: UiObject = video.getChild(
            UiSelector().className("android.widget.Checkbox")
    )
    if (!checkBox.isSelected) checkBox.click()
    

Java

    UiCollection videos = new UiCollection(new UiSelector()
            .className("android.widget.FrameLayout"));

    // Retrieve the number of videos in this collection:
    int count = videos.getChildCount(new UiSelector()
            .className("android.widget.LinearLayout"));

    // Find a specific video and simulate a user-click on it
    UiObject video = videos.getChildByText(new UiSelector()
            .className("android.widget.LinearLayout"), "Cute Baby Laughing");
    video.click();

    // Simulate selecting a checkbox that is associated with the video
    UiObject checkBox = video.getChild(new UiSelector()
            .className("android.widget.Checkbox"));
    if(!checkBox.isSelected()) checkbox.click();
    

对可滚动视图执行操作

使用 UiScrollable 类模拟显示屏上的垂直或水平滚动。当界面元素位于屏幕外而您需要滚动屏幕以使其进入视野时,此方法很有用。

以下代码段展示了如何模拟向下滚动“设置”菜单并点击“关于平板电脑”选项的操作:

Kotlin

    val settingsItem = UiScrollable(UiSelector().className("android.widget.ListView"))
    val about: UiObject = settingsItem.getChildByText(
            UiSelector().className("android.widget.LinearLayout"),
            "About tablet"
    )
    about.click()
    

Java

    UiScrollable settingsItem = new UiScrollable(new UiSelector()
            .className("android.widget.ListView"));
    UiObject about = settingsItem.getChildByText(new UiSelector()
            .className("android.widget.LinearLayout"), "About tablet");
    about.click();
    

验证结果

InstrumentationTestCase 扩展了 TestCase,因此您可以使用标准的 JUnit Assert 方法测试应用中的界面组件是否会返回预期结果。

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

Kotlin

    private const val CALC_PACKAGE = "com.myexample.calc"

    fun testTwoPlusThreeEqualsFive() {
        // Enter an equation: 2 + 3 = ?
        device.findObject(UiSelector().packageName(CALC_PACKAGE).resourceId("two")).click()
        device.findObject(UiSelector().packageName(CALC_PACKAGE).resourceId("plus")).click()
        device.findObject(UiSelector().packageName(CALC_PACKAGE).resourceId("three")).click()
        device.findObject(UiSelector().packageName(CALC_PACKAGE).resourceId("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(new UiSelector()
                .packageName(CALC_PACKAGE).resourceId("two")).click();
        device.findObject(new UiSelector()
                .packageName(CALC_PACKAGE).resourceId("plus")).click();
        device.findObject(new UiSelector()
                .packageName(CALC_PACKAGE).resourceId("three")).click();
        device.findObject(new UiSelector()
                .packageName(CALC_PACKAGE).resourceId("equals")).click();

        // Verify the result = 5
        UiObject result = device.findObject(By.res(CALC_PACKAGE, "result"));
        assertEquals("5", result.getText());
    }
    

在设备或模拟器上运行 UI Automator 测试

您可以通过 Android Studio 或命令行运行 UI Automator 测试。请务必在项目中将 AndroidJUnitRunner 指定为默认插桩测试运行程序。

其他资源

如需详细了解如何在 Android 测试中使用 UI Automator,请参阅以下资源。

示例

Codelab