UI Automator 是 Android SDK 自带的界面自动化测试框架,定位在设备级黑盒测试。它与 Espresso 这类需要和应用组件打交道的框架不同,不依赖被测应用的源码,而是通过系统无障碍服务生成的节点树来查找和操作控件。这意味着测试脚本可以跨越应用边界,直接点击系统设置、通知栏、权限弹窗甚至第三方应用界面。理解这一点很关键,因为很多团队在单应用测试里用 Espresso 非常顺手,一旦遇到系统弹窗就开始手动补测,其实这类场景正好可以交给 UI Automator。

使用 UI Automator 时,核心思路是先把设备当作一个整体,通过 UiDevice 获取设备实例,再用 UiSelector 描述目标控件的特征,最后通过 UiObject 完成点击、输入、滑动等动作。整个过程不需要关心 Activity 生命周期,也不需要注入测试代码到目标应用,因此很适合回归测试和兼容性测试。下面从核心组件、环境配置、定位技巧和稳定性策略几个方面展开。
一、UI Automator 的核心组件与执行模型
UI Automator 的核心类并不多,最常用的是 UiDevice、UiSelector、UiObject、UiCollection 和 UiScrollable。UiDevice 代表当前连接的设备或模拟器,提供 pressBack、pressHome、click、swipe、wakeUp 等设备级操作。UiSelector 用来描述控件的查找条件,可以按 resourceId、text、description、className、packageName 等属性组合筛选。UiObject 则是查找到的一个具体控件,拥有 click、setText、longClick、swipe 等交互方法。
执行模型基于 Instrumentation 测试环境,测试代码运行在单独的测试进程中,通过系统服务的 Accessibility 接口获取窗口节点。由于节点信息是异步刷新的,UI Automator 在查找控件时会有一个默认等待时间,但默认值往往不够用,实际脚本里通常需要显式等待。与 Espresso 自动同步主线程不同,UI Automator 不会自动感知界面是否稳定,所以如果页面还在动画或加载中,直接 findObject 很可能得到 null,后续操作就会抛异常。这一点是不少用例偶发失败的根本原因。
另一个容易混淆的概念是 UiObject 与 UiObject2。UiObject 属于传统 API,使用 UiSelector 构建,功能直观但存在一些限制;UiObject2 来自 AndroidX 测试库的新版 API,基于 BySelector 和 SearchCondition,支持更灵活的等待和条件组合。目前新项目建议优先使用 UiObject2,但大量既有资料和旧代码仍然使用 UiObject,因此两种写法都需要了解。
二、在 Gradle 工程中集成并编写第一个测试
要使用 UI Automator,首先在 Android 项目的 app 模块下添加测试依赖。AndroidX 提供的 uiautomator 库独立于 Android SDK 版本,适合在真机和模拟器上运行。在 build.gradle 文件中加入以下配置即可。
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' androidTestImplementation 'androidx.test:runner:1.5.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5'
依赖添加完成后,在 androidTest 目录下创建测试类。下面这个例子演示了启动应用、输入账号密码并点击登录按钮的完整流程。注意资源 ID 要在被测应用的包名中找,不能直接使用测试项目的 R 文件,因为 UI Automator 是黑盒测试。
@RunWith(AndroidJUnit4.class)
public class LoginUiTest {
@Test
public void loginWithValidCredentials() {
UiDevice device = UiDevice.getInstance(
InstrumentationRegistry.getInstrumentation());
Context context = InstrumentationRegistry.getInstrumentation()
.getTargetContext();
Intent intent = context.getPackageManager()
.getLaunchIntentForPackage("com.example.app");
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
context.startActivity(intent);
UiObject username = device.findObject(
new UiSelector().resourceId("com.example.app:id/username"));
username.waitForExists(5000);
username.setText("test_user");
UiObject password = device.findObject(
new UiSelector().resourceId("com.example.app:id/password"));
password.waitForExists(5000);
password.setText("123456");
UiObject loginButton = device.findObject(
new UiSelector().text("登录"));
loginButton.waitForExists(3000);
loginButton.click();
UiObject homeTitle = device.findObject(
new UiSelector().text("首页"));
assertTrue("登录后未进入首页", homeTitle.waitForExists(5000));
}
}
这个用例先通过包名启动 Activity,然后按 resourceId 找到用户名和密码输入框,再按文本找到登录按钮。最后用 waitForExists 判断登录是否成功。waitForExists 会阻塞当前线程直到控件出现或超时,比直接 findObject 再判断 null 要可靠。但需要注意的是,setText 方法要求目标控件已经获得焦点,如果输入框不在可输入状态,操作可能无效,这时可以先调用 click 再输入。
运行用例时,使用 Gradle 任务 connectedDebugAndroidTest,连接真机或启动模拟器即可。测试报告会输出在 build/reports/androidTests/connected 目录下。若在 Windows 环境中开发,注意 SDK 路径通常包含反斜杠,比如 C:\Android\sdk\platform-tools,配置环境变量时不要误写成斜杠。
三、控件定位技巧与动态列表处理
resourceId 和 text 是最常用的定位条件,但在很多实际界面中,控件可能没有稳定的 resourceId,或者文本内容会随业务状态变化。这时可以使用 UiSelector 的多个方法组合筛选,例如 className 加 instance 索引,或者 textContains 做模糊匹配。UiSelector 的方法支持链式调用,每个方法都会缩小候选范围。
UiObject item = device.findObject(
new UiSelector().className("android.widget.TextView")
.textContains("订单")
.instance(1));
item.click();
上面代码先限制控件类型为 TextView,再匹配包含订单二字的文本,最后取第二个匹配项。instance 从 0 开始计数,适合处理多个相同特征的控件。不过 instance 索引比较脆弱,界面改版后可能导致定位到错误控件。更推荐使用 UiCollection 或 UiScrollable 结合父子关系定位,或者请开发在关键控件上添加固定的 resourceId。
动态列表是自动化测试中的一大难点。对于需要滚动才能看到的列表项,直接 findObject 往往找不到,这时应该使用 UiScrollable。它可以指定滚动容器,并通过 scrollIntoView 或 scrollTextIntoView 自动滚动到目标控件。
UiScrollable list = new UiScrollable(
new UiSelector().resourceId("com.example.app:id/order_list"));
UiObject target = list.getChildByText(
new UiSelector().className("android.widget.TextView"),
"已发货");
target.click();
如果列表控件没有明确的 resourceId,可以用 scrollable(true) 或 className(RecyclerView) 来找到滚动容器。需要注意的是,RecyclerView 的节点信息不像 ListView 那样完整,部分 item 在屏幕外时不会出现在节点树中,因此 UiScrollable 的滚动步长和超时时间需要结合实际页面调整。新版 API 的 UiObject2 配合 By.scrollable 和 Until.scrollUntil 能更精细地控制滚动方向。
四、系统弹窗、权限对话框与跨应用测试
跨应用操作是 UI Automator 最典型的优势场景。当测试流程涉及系统权限弹窗、安装确认框或跳转到系统设置时,Espresso 无法定位这些系统窗口,而 UI Automator 可以。比如首次启动应用会弹出存储权限请求,弹窗上的允许按钮属于系统进程,仍然可以通过 UiSelector 找到。
UiObject allowButton = device.findObject(
new UiSelector().text("允许"));
if (allowButton.exists()) {
allowButton.click();
}
这里使用 exists 而不是 waitForExists,是因为权限弹窗不一定每次都出现。先在条件分支中判断,能避免用例因为找不到按钮而失败。但很多权限弹窗的按钮文本在不同 Android 版本或不同厂商系统上不一致,有的叫允许,有的叫始终允许,还有的系统使用资源 ID。针对这种差异,可以封装一个处理权限弹窗的工具方法,同时匹配多个文本并捕获异常。
另一个常见场景是从测试应用跳转到系统设置页,例如关闭通知开关或修改定位权限。UI Automator 可以直接操作设置界面。下面代码演示如何打开应用详情页并点击权限入口。
Intent detailIntent = new Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.parse("package:com.example.app"));
detailIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(detailIntent);
UiObject permissionEntry = device.findObject(
new UiSelector().text("权限"));
permissionEntry.waitForExists(5000);
permissionEntry.click();
这类跨应用用例需要额外注意系统版本和 ROM 差异,原生 Android 和 MIUI、EMUI 等定制系统的设置页结构可能完全不同。如果团队需要覆盖多厂商设备,建议用 device.getCurrentPackageName() 先判断当前界面是否在设置应用,或者使用 resourceId 匹配厂商特定的控件 ID。UI Automator 本身无法消除系统差异,只能通过更灵活的定位策略来适配。
五、等待策略与 UiWatcher 提升稳定性
固定 sleep 是自动化测试中最常见的反模式。比如在点击之后写 Thread.sleep(3000),虽然简单,但会让用例变慢且不稳定。UI Automator 提供了更好的等待机制,Wait 类和 Until 条件可以轮询查找控件直到满足条件或超时。下面代码等待一个包含指定文本的按钮出现,而不是固定等待。
UiDevice device = UiDevice.getInstance(
InstrumentationRegistry.getInstrumentation());
UiObject button = device.findObject(
new UiSelector().text("提交"));
boolean appeared = button.waitForExists(10000);
assertTrue("提交按钮未在10秒内出现", appeared);
除了 waitForExists,新版 UiObject2 的 wait 方法支持传入 SearchCondition,比如 Until.findObject 和 Until.gone,可以等待控件消失,这在处理加载动画时非常有用。例如等待进度条消失后再执行断言,比 sleep 更符合真实用户操作节奏。
UiObject2 loader = device.findObject(
By.res("com.example.app:id/loading"));
loader.wait(Until.gone(By.res("com.example.app:id/loading")), 10000);
UiWatcher 是处理突发弹窗的利器。它可以在测试过程中注册一个监听器,当查找控件失败时自动触发,通常用来点击偶发的广告弹窗、升级提示或崩溃恢复对话框。注册后 UiWatcher 会在后台异步检查,如果发现匹配的控件就执行指定操作。比如应用突然弹出升级提醒,UiWatcher 可以自动点击暂不升级,保证主流程继续执行。
device.registerWatcher("upgrade_dialog", () -> {
UiObject dismiss = device.findObject(
new UiSelector().text("暂不升级"));
if (dismiss.exists()) {
dismiss.click();
return true;
}
return false;
});
上面的 Lambda 代码需要 Java 8 支持,如果是旧项目可以在 build.gradle 中启用 coreLibraryDesugaring。注册 UiWatcher 后要记得在用例结束前调用 removeWatcher,否则可能影响后续测试。合理的等待策略加上 UiWatcher,可以让 UI Automator 用例在真机运行时的通过率明显提升,减少因环境干扰造成的误报。
UI AutomatorAndroid自动化测试界面测试修改时间:2026-10-05 08:06:30