24、UI测试:Espresso测试认证流程、模拟用户交互、验证UI状态
说实话,很多开发者写完了虹膜认证的代码,跑一遍功能没问题就交差了。但我个人的经验是——UI层才是最容易翻车的地方。你想想看,底层算法再牛,如果界面上弹了个奇怪的对话框,或者用户点了一下取消按钮就崩溃了,那体验直接归零。
所以这一章,我们来聊聊怎么用 Espresso 给虹膜认证的 UI 做自动化测试。说白了,就是让机器替我们点来点去,然后检查界面是不是按预期在走。
为什么是 Espresso?
Android 官方推荐的 UI 测试框架有好几个,但 Espresso 是我用得最顺手的。原因很简单:它跟你的 App 跑在同一个进程里,不需要等待跨进程通信,测试跑起来飞快。
我记得有一次在项目里用 UiAutomator 测虹膜弹窗,每次启动都要等好几秒。换成 Espresso 之后,整个测试套件跑完的时间缩短了将近一半。嗯,这里有个前提——你得用对它的同步机制。
认证流程的测试设计
虹膜认证的 UI 流程,通常长这样:
- 用户点击「开始认证」按钮
- 系统弹出虹膜识别界面
- 用户对准摄像头(模拟)
- 认证成功/失败,显示结果
我们要测的,就是每一步的 UI 状态是否正确。我习惯把测试用例分成三类:
- 正常流程:用户按预期操作,认证成功
- 异常流程:用户取消、超时、识别失败
- 边界情况:快速点击、重复认证、屏幕旋转
核心思路:不要只测「成功」路径。我在项目中踩过最大的坑,就是用户连续点了两次「取消」按钮,结果界面卡死了。这种问题,不写 UI 测试根本发现不了。
模拟用户交互
Espresso 模拟交互的方式非常直观。比如我们要模拟用户点击「开始认证」按钮:
onView(withId(R.id.btn_start_auth))
.perform(click());
就这么一行。但如果你要模拟更复杂的操作,比如输入 PIN 码或者滑动滑块,可以这样:
onView(withId(R.id.pin_entry))
.perform(typeText("123456"), closeSoftKeyboard());
onView(withId(R.id.slider_iris))
.perform(swipeRight());
我个人习惯把常用的交互封装成工具方法,比如:
public static void startIrisAuth() {
onView(withId(R.id.btn_start_auth))
.check(matches(isDisplayed()))
.perform(click());
}
这样测试用例读起来就像在描述操作步骤,维护起来也方便。
小技巧:用 withId() 定位控件是最稳定的。别用 withText(),万一哪天产品改了文案,你的测试就全挂了。我曾经就因为这个吃过亏。
验证 UI 状态
交互完了,怎么知道界面对不对?用 check() 方法。比如验证认证成功的提示是否显示:
onView(withText("认证成功"))
.check(matches(isDisplayed()));
还可以验证按钮是否可用、列表项数量、甚至某个 View 的可见性:
// 验证取消按钮不可点击
onView(withId(R.id.btn_cancel))
.check(matches(not(isEnabled())));
// 验证错误提示文本
onView(withId(R.id.tv_error))
.check(matches(withText("虹膜扫描失败,请重试")));
这里有个容易忽略的点——异步操作。虹膜识别通常需要几百毫秒甚至更久,Espresso 默认会等待主线程空闲。但如果你的识别逻辑跑在后台线程,就需要用 IdlingResource 来告诉 Espresso:「嘿,后台还在忙,别急着检查 UI」。
@Test
public void testIrisAuthSuccess() {
// 注册空闲资源
IdlingResource idling = new ElapsedTimeIdlingResource(2000);
Espresso.registerIdlingResources(idling);
startIrisAuth();
// 模拟识别完成
onView(withId(R.id.iv_iris_scan))
.check(matches(isDisplayed()));
// 注销资源
Espresso.unregisterIdlingResources(idling);
}
注意:别忘了在 @After 方法里注销所有 IdlingResource,否则会影响其他测试用例。我见过有人忘了这步,结果整个测试套件跑完要花三倍时间。
完整的测试用例示例
下面是一个完整的 Espresso 测试用例,覆盖了虹膜认证的「成功」和「取消」两种场景:
@RunWith(AndroidJUnit4.class)
public class IrisAuthUITest {
@Rule
public ActivityTestRule<MainActivity> activityRule =
new ActivityTestRule<>(MainActivity.class);
@Test
public void testAuthSuccess_showsSuccessMessage() {
// 点击开始认证
onView(withId(R.id.btn_start_auth)).perform(click());
// 模拟识别成功(假设有模拟器支持)
onView(withId(R.id.iv_iris_scan)).perform(click());
// 验证成功提示
onView(withText("认证成功"))
.check(matches(isDisplayed()));
}
@Test
public void testCancelAuth_returnsToMain() {
onView(withId(R.id.btn_start_auth)).perform(click());
// 点击取消
onView(withId(R.id.btn_cancel)).perform(click());
// 验证回到了主界面
onView(withId(R.id.btn_start_auth))
.check(matches(isDisplayed()));
}
}
知识体系总览
我把这一章的核心逻辑画成了图,方便你对照理解:
避坑指南
最后分享几个我踩过的坑:
- 不要依赖真实硬件:虹膜识别需要摄像头,但 UI 测试应该在模拟器上跑。用 Mock 对象模拟识别结果,别让测试依赖物理设备。
- 注意权限弹窗:如果测试用例涉及摄像头权限,记得在
@Before里提前授权,否则 Espresso 会卡在系统弹窗上。 - 测试数据隔离:每个测试用例跑完后,清理掉产生的数据。我习惯在
@After里调用activityRule.getActivity().finish(),确保下一个用例从干净状态开始。
嗯,UI 测试这件事,说白了就是「用代码模拟用户,用断言验证结果」。刚开始写可能会觉得麻烦,但等你上线后被用户反馈「点取消就闪退」的时候,你就会感谢当初写了这些测试的自己。