24、UI测试:Espresso测试认证流程、模拟用户交互、验证UI状态

说实话,很多开发者写完了虹膜认证的代码,跑一遍功能没问题就交差了。但我个人的经验是——UI层才是最容易翻车的地方。你想想看,底层算法再牛,如果界面上弹了个奇怪的对话框,或者用户点了一下取消按钮就崩溃了,那体验直接归零。

所以这一章,我们来聊聊怎么用 Espresso 给虹膜认证的 UI 做自动化测试。说白了,就是让机器替我们点来点去,然后检查界面是不是按预期在走。

为什么是 Espresso?

Android 官方推荐的 UI 测试框架有好几个,但 Espresso 是我用得最顺手的。原因很简单:它跟你的 App 跑在同一个进程里,不需要等待跨进程通信,测试跑起来飞快。

我记得有一次在项目里用 UiAutomator 测虹膜弹窗,每次启动都要等好几秒。换成 Espresso 之后,整个测试套件跑完的时间缩短了将近一半。嗯,这里有个前提——你得用对它的同步机制。

认证流程的测试设计

虹膜认证的 UI 流程,通常长这样:

  1. 用户点击「开始认证」按钮
  2. 系统弹出虹膜识别界面
  3. 用户对准摄像头(模拟)
  4. 认证成功/失败,显示结果

我们要测的,就是每一步的 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()));
    }
}

知识体系总览

我把这一章的核心逻辑画成了图,方便你对照理解:

Espresso 虹膜认证 UI 测试体系 正常流程测试 异常流程测试 边界情况测试 模拟用户交互(click / typeText / swipeRight) 验证 UI 状态(check / matches) 关键:IdlingResource 处理异步 · 封装工具方法 · 避免 withText 硬编码

避坑指南

最后分享几个我踩过的坑:

  • 不要依赖真实硬件:虹膜识别需要摄像头,但 UI 测试应该在模拟器上跑。用 Mock 对象模拟识别结果,别让测试依赖物理设备。
  • 注意权限弹窗:如果测试用例涉及摄像头权限,记得在 @Before 里提前授权,否则 Espresso 会卡在系统弹窗上。
  • 测试数据隔离:每个测试用例跑完后,清理掉产生的数据。我习惯在 @After 里调用 activityRule.getActivity().finish(),确保下一个用例从干净状态开始。

嗯,UI 测试这件事,说白了就是「用代码模拟用户,用断言验证结果」。刚开始写可能会觉得麻烦,但等你上线后被用户反馈「点取消就闪退」的时候,你就会感谢当初写了这些测试的自己。


公众号:蓝海资料掘金营,微信deep3321