UI Automator 是一个界面测试框架,适用于跨系统和已安装的应用进行跨应用功能界面测试。借助 UI Automator API,您可以与设备上的可见元素进行交互(无论焦点是哪个 Activity
),从而能够在测试设备中执行打开“设置”菜单或应用启动器等操作。测试可以使用方便的描述符(例如,显示在相应组件中的文本或其内容说明)来查找界面组件。
UI Automator 测试框架是基于插桩的 API,可与 AndroidJUnitRunner
测试运行程序配合使用。它非常适合编写不透明的盒式自动化测试,在此类测试中,测试代码不依赖于目标应用的内部实现细节。
UI Automator 测试框架的主要功能包括:
- 用于在目标设备上检索状态信息并执行操作的 API。如需了解详情,请参阅访问设备状态。
- 支持跨应用界面测试的 API。如需了解详情,请参阅 UI Automator API。
访问设备状态
UI Automator 测试框架提供了一个 UiDevice
类,用于在运行目标应用的设备上访问和执行操作。您可以调用其方法来访问设备属性,例如当前屏幕方向或显示屏尺寸。借助 UiDevice
类,您还可以执行以下操作:
- 改变设备的旋转。
- 按硬件键,如“音量调高”。
- 按返回、主屏幕或菜单按钮。
- 打开通知栏。
- 截取当前窗口的屏幕截图。
例如,如需模拟按下主屏幕按钮的操作,请调用 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
工具,请执行以下操作:
- 在实体设备上启动目标应用。
- 将设备连接到开发机器。
- 打开终端窗口并导航至
<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() 方法获取
UiObject2
对象,以访问设备上显示的界面组件(例如,前台的当前视图)。 - 通过调用
UiObject2
方法,模拟要对该界面组件执行的特定用户互动;例如,调用 scrollUntil() 以滚动页面,调用 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)或更高版本的设备上运行(根据 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()
:向指定方向执行滚动操作,直到满足Condition
或EventCondition
。
借助 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());
等待转换
![关闭干扰](https://developer.android.google.cn/static/images/testing/uiautomator_turn_off_disturb.gif?authuser=5&hl=el)
屏幕转换可能需要一些时间,并且预测其持续时间并不可靠,因此您应该让 UI Automator 在执行操作后等待。UI Automator 提供了多种方法来实现此目的:
UiDevice.performActionAndWait(Runnable action, EventCondition<U> condition, long timeout)
:例如,如需点击某个按钮并等待新窗口显示,请调用device.performActionAndWait(() -> button.click(), Until.newWindow(), timeout)
UiDevice.wait(Condition<Object, U> condition, long timeout)
:例如,如需等待设备上出现特定UiObject2
,请调用device.wait(device.hasObject(By.text("my_text")), timeout);
UiObject2.wait(@NonNull Condition<Object, U> condition, long timeout)
:例如,如需等到复选框被选中,请调用checkbox.wait(Until.checked(true), timeout);
UiObject2.clickAndWait(@NonNull EventCondition<U> condition, long timeout)
:例如,如需点击某个按钮并等待新窗口显示,请调用button.clickAndWait(Until.newWindow(), timeout);
UiObject2.scrollUntil(@NonNull Direction direction, @NonNull Condition<Object, U> condition)
:例如,如需向下滚动直到出现一个新对象,请调用object.scrollUntil(Direction.DOWN, Until.hasObject(By.text('new_obj')));
UiObject2.scrollUntil(@NonNull Direction direction, @NonNull EventCondition<U> condition)
:例如,如需向下滚动到底部,请调用object.scrollUntil(Direction.DOWN, Until.scrollFinished(Direction.DOWN));
以下代码段展示了如何使用 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,请参阅以下资源。
参考文档:
示例
- BasicSample:基本的 UI Automator 示例。