4、Espresso框架入门:核心概念与实战三件套

好,咱们进入正题。Espresso 是什么?说白了,它就是 Google 官方推出的 Android UI 自动化测试框架。我当年刚接触它的时候,第一反应是——这玩意儿写测试居然能这么直观?不用 sleep,不用等,代码还特别简洁。

今天这一章,我会带你彻底搞懂 Espresso 的四个核心概念:onView 与 onData、匹配器(Matcher)、操作(ViewAction)、断言(ViewAssertion)。掌握了这些,你就能写出稳定、高效的 UI 测试用例。

4.1 Espresso 的核心思想

Espresso 的设计哲学很简单:模拟用户操作,验证界面反馈。它自动处理了线程同步,你不需要手动加延迟。我个人觉得,这是它比老一代框架强的地方。

它的工作流程可以概括为三步:

  1. 找到控件 —— 用 onView 或 onData
  2. 执行操作 —— 用 ViewAction,比如点击、输入
  3. 验证结果 —— 用 ViewAssertion,比如检查文本

嗯,就是这么简单。但实际项目中,坑往往出在第一步——控件找不到。

4.2 onView:最常用的控件查找方式

onView() 是 Espresso 里最常用的入口方法。它接收一个 Matcher(匹配器),返回一个 ViewInteraction 对象。然后你就可以在这个对象上调用 .perform().check()

来看一个最基础的例子:

// 找到 id 为 button_submit 的按钮,点击它
onView(withId(R.id.button_submit))
    .perform(click());

// 检查 TextView 显示的内容是否正确
onView(withId(R.id.text_result))
    .check(matches(withText("登录成功")));

这里 withId() 就是一个匹配器。Espresso 提供了很多现成的匹配器,比如 withText()withContentDescription()isDisplayed() 等等。

我的习惯:能用 withId 就用 withId。它最稳定,不会因为文本变化或布局调整而失效。我在项目中见过太多因为用 withText 匹配导致测试用例脆弱的案例了。

4.3 onData:处理列表和 AdapterView

如果你的控件在 ListView、GridView 或 Spinner 里,onView 可能就不好使了。因为这些控件只渲染屏幕上可见的部分。这时候就要用 onData()

onData() 匹配的是 数据层,而不是视图层。它接收一个 Matcher,匹配 Adapter 里的数据对象。

举个例子:

// 在 ListView 中找到包含 "苹果" 的数据项,点击它
onData(allOf(
    is(instanceOf(String.class)),
    is("苹果")
)).inAdapterView(withId(R.id.fruit_list))
  .perform(click());

这里 inAdapterView() 指定了目标 AdapterView 的 id。onData 会自动滚动列表,找到匹配的数据项。

我曾经踩过的坑:onData 匹配的是数据对象,不是视图。如果你在 Adapter 里对数据做了转换(比如拼接字符串),那匹配器也要跟着调整。否则测试会一直报 NoMatchingViewException,排查起来很头疼。

4.4 匹配器(Matcher):精准定位控件

匹配器是 Espresso 的灵魂。它决定了你能不能准确找到目标控件。Espresso 基于 Hamcrest 匹配器框架,所以你可以组合使用各种匹配器。

常用的匹配器我整理了一张表:

匹配器 作用 示例
withId() 按资源 ID 匹配 withId(R.id.button_ok)
withText() 按文本内容匹配 withText("确定")
withContentDescription() 按内容描述匹配(无障碍) withContentDescription("搜索")
isDisplayed() 控件是否可见 isDisplayed()
isCompletelyDisplayed() 控件是否完全可见 isCompletelyDisplayed()
hasSibling() 按兄弟控件匹配 hasSibling(withText("密码"))
allOf() 同时满足多个条件 allOf(withId(...), withText(...))
anyOf() 满足任意一个条件 anyOf(withText("是"), withText("否"))

你想想看,实际项目中一个页面可能有十几个控件。如果只用 withId,有时候会匹配到多个(比如 RecyclerView 的 item)。这时候 allOfhasSibling 就派上用场了。

// 找到 "密码" 标签旁边的 EditText
onView(allOf(
    withId(R.id.edit_text),
    hasSibling(withText("密码"))
)).perform(typeText("123456"));
核心要点:匹配器越精确,测试越稳定。不要偷懒只用一个匹配条件。我建议至少用两个条件组合,比如 id + text,或者 id + isDisplayed。

4.5 操作(ViewAction):模拟用户行为

找到控件之后,就要对它执行操作。Espresso 提供了丰富的 ViewAction,通过 .perform() 方法调用。

常用的操作有:

  • click() —— 点击
  • typeText() —— 输入文本
  • clearText() —— 清空文本
  • scrollTo() —— 滚动到控件可见
  • pressBack() —— 按下返回键
  • closeSoftKeyboard() —— 关闭软键盘

看一个完整的输入流程:

// 先清空,再输入,最后关闭键盘
onView(withId(R.id.edit_username))
    .perform(clearText(), typeText("admin"), closeSoftKeyboard());

onView(withId(R.id.edit_password))
    .perform(clearText(), typeText("123456"), closeSoftKeyboard());

onView(withId(R.id.button_login))
    .perform(click());

这里 .perform() 可以接收多个 ViewAction,按顺序执行。我个人习惯把输入和键盘关闭写在一起,避免键盘遮挡后续的点击操作。

避坑指南:typeText 会直接设置文本,不会逐字模拟输入。如果你需要测试输入法联想或自动补全功能,可以用 replaceText() 或者自定义 ViewAction。我曾经在输入框的 TextWatcher 上吃过亏,typeText 触发的回调顺序和真实输入不一样。

4.6 断言(ViewAssertion):验证测试结果

操作执行完了,怎么知道结果对不对?用断言。Espresso 的断言通过 .check() 方法调用,最常用的是 matches()

常见的断言场景:

  • 检查文本:matches(withText("成功"))
  • 检查可见性:matches(isDisplayed())
  • 检查是否选中:matches(isChecked())
  • 检查是否可点击:matches(isClickable())
  • 检查控件是否存在:doesNotExist()

来看一个完整的测试用例:

@Test
public void testLoginSuccess() {
    // 输入用户名和密码
    onView(withId(R.id.edit_username))
        .perform(typeText("admin"), closeSoftKeyboard());
    onView(withId(R.id.edit_password))
        .perform(typeText("123456"), closeSoftKeyboard());

    // 点击登录按钮
    onView(withId(R.id.button_login))
        .perform(click());

    // 验证登录成功提示
    onView(withId(R.id.text_result))
        .check(matches(withText("登录成功")));

    // 验证跳转到主页
    onView(withId(R.id.main_container))
        .check(matches(isDisplayed()));
}

嗯,这里要注意一点:doesNotExist()matches(not(isDisplayed())) 是有区别的。前者是控件根本不在视图树里,后者是控件存在但不可见。我建议根据业务场景选择。

4.7 组合使用:一个实战案例

最后,咱们把今天学的知识串起来,写一个稍微复杂点的测试。假设有一个购物 App 的搜索功能:

@Test
public void testSearchAndAddToCart() {
    // 1. 点击搜索框
    onView(withId(R.id.search_icon))
        .perform(click());

    // 2. 输入关键词并搜索
    onView(withId(R.id.search_input))
        .perform(typeText("手机"), pressSearchKey());

    // 3. 在搜索结果列表中,找到第一个商品
    onData(allOf(
        is(instanceOf(Product.class)),
        hasEntry(equalTo("name"), containsString("手机"))
    )).inAdapterView(withId(R.id.search_results))
      .atPosition(0)
      .perform(click());

    // 4. 验证商品详情页显示
    onView(withId(R.id.product_detail))
        .check(matches(isDisplayed()));

    // 5. 点击加入购物车
    onView(withId(R.id.add_to_cart_button))
        .perform(click());

    // 6. 验证购物车角标更新
    onView(withId(R.id.cart_badge))
        .check(matches(withText("1")));
}

这个用例涵盖了 onView、onData、多种匹配器、操作和断言。你想想看,如果不用 Espresso,用传统的 sleep + findView 方式,这个测试得写多长?而且还不稳定。

总结一下:Espresso 的核心就是「找控件 → 做操作 → 验结果」。匹配器要精准,操作要合理,断言要明确。把这三点做好,你的 UI 测试就稳了。

下一章,我会带你深入 IdlingResource,解决异步操作和网络请求的同步问题。这可是 Espresso 进阶的关键,别错过。