3. Espresso 基础:onView、onData、ViewMatchers、ViewActions、ViewAssertions 使用详解

Espresso 是 Android 官方测试框架里最趁手的工具之一。说实话,刚开始接触的时候我也觉得 API 挺多的,但用顺手之后,你会发现它的设计思路非常清晰——就是「找控件,做操作,验结果」这三步。今天我就按这个套路,把核心的几个组件跟你聊透。

Espresso 核心组件与流程 Espresso 测试三部曲 onView / onData ViewMatchers → ViewActions ViewAssertions withId() / withText() / hasSibling() click() / typeText() / scrollTo() matches() / doesNotExist() 支持 Compose 混合测试

3.1 起手式:onView 和 onData

onView 是 Espresso 的入口函数,专门用来定位屏幕上的视图控件。它接收一个 ViewMatcher 参数,返回一个 ViewInteraction 对象。我个人的习惯是 —— 能通过 withId 定位就优先用它,因为 id 是唯一且稳定的。

那 onData 呢?它用来处理 AdapterView 里面的数据项,比如 ListView、GridView、Spinner。说白了,当你需要操作列表里某一行的元素,而不是整个列表容器时,就得靠 onData 上场了。我在项目中曾经踩过一个坑:直接用 onView 去匹配 ListView 的子 View,结果总是找不到。后来才意识到,对于 RecyclerView 需要用 onView,但对于老式的 AdapterView,必须用 onData 才能命中数据层。

核心区别

  • onView:基于视图层级结构查找,适合大多数静态布局以及 RecyclerView。
  • onData:基于适配器中的数据模型查找,适合 AdapterView 系控件。

3.2 ViewMatchers:怎么精确描述你要找的控件

ViewMatchers 本质上是一堆函数,用来描述控件的特征。Espresso 内置了 50+ 匹配器,常用的其实就下面这几个。

匹配器作用示例
withId(R.id.my_button)按资源 ID 匹配最常用,保证唯一性
withText("确定")按显示的文本匹配注意字符串可能随语言变化
hasSibling(withText("用户名"))匹配有特定兄弟节点的控件适合表单中的输入框
isDisplayed()控件可见且正在显示通常用 check(matches(isDisplayed()))
allOf(matcher1, matcher2, …)组合多个条件(且)allOf(withId(...), withText(...))
anyOf(matcher1, matcher2, …)满足任一条件(或)较少用,但偶尔有奇效
小窍门:当页面树太深、控件 id 重复时,试试 allOf 搭配 hasSibling 或 isDescendantOfA(parentMatcher)。我曾经用这个方法解决过一个复杂商品列表的定位问题,一次就通过了。

3.3 ViewActions:模拟用户操作

找准控件之后,下一步就是模拟操作。Espresso 提供的操作几乎覆盖了所有常见手势,我把最常用列出来。

  • click() —— 点击,最基础。但我提醒你一句:如果要连续点击两次,别用 click().click(),而应该链式调用 perform(click(), click()) 或直接用 doubleClick(),否则可能会因为同步问题丢操作。
  • typeText("Hello") —— 输入文本。它会先清空 EditText 再输入,比直接 replaceText 更安全。
  • clearText() —— 清空内容。
  • scrollTo() —— 滚动到目标控件可见。但注意:如果控件本身在屏幕内,调用 scrollTo 反而可能失败——因为 Espresso 认为没必要滚动。
  • pressBack() —— 按下返回键。
// 一个完整的登录测试例子
onView(withId(R.id.username))
    .perform(typeText("testuser"));
onView(withId(R.id.password))
    .perform(typeText("123456"));
onView(withId(R.id.login_button))
    .perform(click());

避坑提示:我曾经写过 perform(typeText("abc"), click()) 试图在一个控件上连续执行两个动作。结果发现第二个动作常常不生效。正确的做法是:一次 perform 内可以放多个动作,但一定要确保它们都是该控件能接受的。如果你需要先输入再点击另一个按钮,必须分两次 perform。

3.4 ViewAssertions:验证结果

怎么知道操作有没有生效?靠断言。Espresso 的断言方法就一个 check(ViewAssertion),但配合不同的 Matcher 可以玩出各种花样。

  • matches(withText("成功")) —— 最常用,验证控件显示特定文本。
  • doesNotExist() —— 验证控件从视图树上消失。比如登录成功后加载指示器应该消失。
  • isNotChecked() —— 验证 CheckBox 是否未勾选。
  • matches(isEnabled()) —— 验证按钮是否可点击。
// 验证登录成功后跳转到了主页,且隐藏了进度条
onView(withId(R.id.error_message))
    .check(matches(doesNotExist()));
onView(withId(R.id.welcome_text))
    .check(matches(withText("欢迎回来")));

小经验:我习惯在写断言之前先加一个 .check(matches(isDisplayed())) 确保控件真的可见。不然有时候测试报告说找不到控件,其实控件压根没渲染出来。

3.5 混合测试中的注意事项

现在很多项目都是 Compose 和传统 View 混用。Espresso 默认只认识 Android 原生 View,遇到 Compose 的 ComposeView 或者直接是 Composable 函数,你需要用 ComposeTestRule 或者通过 onView 定位到包裹 Compose 内容的容器。具体怎么做,后面有专章讲。这里先记住一点:

  • 如果目标控件是 Compose 写的,用 composeTestRule.onNodeWithText(...)。
  • 如果目标控件是 View,继续用 onView(...)。
  • 在一个 Activity 里两者并存时,混合使用完全没问题——Espresso 和 Compose 的测试 API 是独立但兼容的。

嗯,这一章的核心就是把「三部曲」练熟。找控件可以用 onView 或 onData,描述控件用 Matchers,操作用 Actions,验证用 Assertions。你想想看,是不是比用手点来点去靠谱多了?