通过涉及多个应用中的用户交互的界面测试,您可以验证当用户流跨入其他应用或系统界面时,您的应用是否能够正常运行。短信应用就是此类用户流的一个例子,该应用先让用户输入短信,再启动 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
工具,请执行以下操作:
- 在实体设备上启动目标应用。
- 将设备连接到开发机器。
- 打开终端窗口并导航至
<android-sdk>/tools/
目录。 - 使用以下命令运行该工具:
$ uiautomatorviewer
如需查看应用的界面属性,请执行以下操作:
- 在
uiautomatorviewer
界面中,点击 Device Screenshot 按钮。 - 将鼠标悬停在左侧面板中的快照上,以查看
uiautomatorviewer
工具识别的界面组件。右下方的面板中列出了属性,右上方的面板中列出了布局层次结构。 - (可选)点击 Toggle NAF Nodes 按钮,以查看 UI Automator 无法访问的界面组件。对于这些组件,系统显示的相关信息可能很有限。
如需了解 Android 提供的常见类型的界面组件,请参阅界面。
确保 Activity 可访问
UI Automator 测试框架在已实现 Android 无障碍功能的应用上效果更好。当您使用类型为 View
或 SDK 中的 View
的子类的界面元素时,无需实现无障碍功能支持,因为这些类已经为您实现了这项支持。
不过,某些应用会使用自定义界面元素来提供更丰富的用户体验。此类元素不会提供自动无障碍功能支持。如果您的应用包含不是 SDK 中的 View
的子类的实例,请务必向这些元素添加无障碍功能,具体操作步骤如下:
- 创建一个扩展
ExploreByTouchHelper
的具体类。 - 通过调用
setAccessibilityDelegate()
,将新类的实例与特定自定义界面元素相关联。
如需获得有关向自定义视图元素添加无障碍功能的其他指导,请参阅构建无障碍自定义视图。如需详细了解 Android 平台上无障碍功能的常规最佳做法,请参阅改进应用的无障碍功能。
创建 UI Automator 测试类
UI Automator 测试类的编写方式应与 JUnit 4 测试类相同。如需详细了解如何创建 JUnit 4 测试类以及如何使用 JUnit 4 断言和注释,请参阅创建插桩单元测试类。
在测试类定义的开头添加 @RunWith(AndroidJUnit4.class)
注释。您还需要将 AndroidX Test 中提供的 AndroidJUnitRunner
类指定为默认测试运行程序。在设备或模拟器上运行 UI Automator 测试部分对此步骤进行了更详细的说明。
在 UI Automator 测试类中实现以下编程模型:
- 通过调用
getInstance()
方法并将Instrumentation
对象作为参数传递给该方法,获取UiDevice
对象以访问要测试的设备。 - 通过调用
findObject()
方法,获取UiObject
对象以访问设备上显示的界面组件(例如,前台的当前视图)。 - 通过调用
UiObject
方法,模拟需要在该界面组件上执行的特定用户交互;例如,调用performMultiPointerGesture()
以模拟多点触控手势,以及调用setText()
以修改文本字段。您可以根据需要反复调用第 2 步和第 3 步中的 API,以测试涉及多个界面组件或用户操作序列的更复杂的用户交互。 - 执行这些用户交互后,检查界面是否反映了预期的状态或行为。
下面几部分更详细地介绍了这些步骤。
访问界面组件
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
类中的方法,在由该对象表示的界面组件上执行用户交互。您可以指定如下操作:
-
click()
:点击界面元素的可见边界的中心。 -
dragTo()
:将此对象拖动到任意坐标。 -
setText()
:清除可修改字段的内容后,设置该字段中的文本。相反,clearTextField()
方法用于清除可修改字段中的现有文本。 -
swipeUp()
:对UiObject
执行向上滑动操作。同样,swipeDown()
、swipeLeft()
和swipeRight()
方法用于执行相应的操作。
通过 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,请参阅以下资源。
示例
- BasicSample:基本的 UI Automator 示例。