4. ViewMatchers 基础归纳总结

学完了前几章,你肯定已经能写出简单的 Espresso 测试了。但说实话,真正决定测试写得好不好的关键,就是你能不能精准定位到目标 View。而定位的基础,就是 ViewMatchers。

这一章我们来系统地梳理一下 ViewMatchers 的常用匹配器、组合方式,以及我这些年踩过的坑。相信我,把这些基础打牢,你后面写自定义匹配器时会轻松很多。

4.1 什么是 ViewMatchers?

说白了,ViewMatchers 就是一套用来描述“你要找什么样的视图”的规则。Espresso 会在视图树里遍历所有 View,然后拿你的规则去比对,符合的就返回给你。

比如你告诉它:“我要找一个 id 为 R.id.submit 的按钮”,它就会帮你找到那个按钮。匹配器可以是一个条件,也可以是多个条件的组合。

核心思想:匹配器 = 一个 Matcher<View> 对象,决定“哪些 View 符合条件”。

4.2 常用内置匹配器一览

Espresso 提供了很多现成的匹配器,基本覆盖了 90% 的日常场景。我习惯把它们分成三类:属性匹配、状态匹配和文本匹配。

类别 匹配器 作用 常用示例
属性匹配 withId(int id) 匹配指定资源 ID 的 View withId(R.id.button)
withText(String text) 匹配显示指定文本的 View(支持 String / 资源 ID) withText("确定")
withTag(Object tag) 匹配设置了指定 tag 的 View withTag("header_tag")
withContentDescription(String desc) 匹配无障碍描述 withContentDescription("关闭按钮")
withClassName(String className) 匹配指定类名的 View withClassName("android.widget.Button")
withHint(String hint) 匹配 EditText 的 hint withHint("请输入密码")
状态匹配 isDisplayed() View 在屏幕上可见(至少部分可见) isDisplayed()
isCompletelyDisplayed() View 完全显示在屏幕内 isCompletelyDisplayed()
isEnabled() View 处于可交互状态 isEnabled()
isClickable() View 可点击 isClickable()
isFocusable() View 可获得焦点 isFocusable()
isSelected() View 处于选中状态 isSelected()
文本匹配 withText(containsString(...)) 使用 Hamcrest 匹配器匹配部分文本 withText(containsString("登录"))
小技巧:withText() 内部已经支持 Hamcrest 的字符串匹配器(startsWith、containsString 等),但如果你需要更复杂的文本匹配,可以显式传一个 Matcher<String>。

4.3 组合匹配器:allOf / anyOf / not

现实中的 View 往往有多个特征。比如你要找一个“显示‘提交’文字且可点击的按钮”,单靠一个 withText 不够,你得把条件组合起来。

Espresso 借助 Hamcrest 提供了三个核心组合器:

  • allOf:所有条件都满足(逻辑与)
  • anyOf:至少满足一个条件(逻辑或)
  • not:条件取反(逻辑非)

看看代码就明白了:

// 查找 id 为 submit_button 且文本包含“提交”的 View
onView(allOf(withId(R.id.submit_button), withText(containsString("提交"))))
    .perform(click());

// 查找文本为“确定”或“确认”的按钮
onView(anyOf(withText("确定"), withText("确认")))
    .perform(click());

// 查找所有不是 GONE 状态的 View
onView(not(withEffectiveVisibility(View.GONE)))
    .check(matches(isDisplayed()));

我曾经在项目中犯过一个错:用 allOf 把 isDisplayed() 和 withId() 组合在一起,结果发现 isDisplayed() 实际上是对匹配到的 View 再进行一次断言,而不是过滤条件。后来改用 hasDisplayedParent() 才解决。这个坑后面再细说。

4.4 个人最佳实践

我写了两年 Espresso,总结出几条原则:

  1. 优先用 withId。它是性能最好、唯一性最强的匹配器。只要你的 UI 组件有 id,就别用其他匹配器绕弯子。
  2. withText 适合确认内容,不适合定位。尤其是多语言 App,文本翻译一变你的测试就挂了。所以拿 withText 做检查(check(matches(...)))比拿它定位更靠谱。
  3. 善用 anyOf 做兼容。比如同个功能在不同 Android 版本上有不同实现(系统弹窗按钮文本不同),anyOf 就能优雅地兼容。
  4. 避免模糊匹配。用 withText(containsString("登")) 可能同时命中“登录”和“登出”两个按钮——嗯,我亲眼见过同事因为这个调试了一下午。
避坑指南:我踩过的坑
我曾经在测试 RecyclerView 时,想用 withId(R.id.item_title) 获取每一项里的 TextView,结果发现同一个 id 出现了很多次(item 被复用)。withId 本身不保证唯一性,它只是匹配条件。这时候必须配合 inAdapterView 或 atPosition 来缩小范围。

4.5 匹配器结构总览

把上面的内容画成一张图,你会看得更清楚。ViewMatchers 体系大致如下:

ViewMatchers 基础结构图 ViewMatchers 基础分类 ViewMatchers 属性匹配 Attribute 状态匹配 State 组合逻辑 Combinator withId withText withTag withHint isDisplayed isEnabled isClickable isSelected allOf anyOf not 提示:每个叶子节点都可以与 自定义匹配器 组合使用,后续章节详解

4.6 快速记忆口诀

最后分享一个我自己的记忆方法,方便你脑子里快速检索:

  • 找特征 -> withId / withText / withTag
  • 查状态 -> isDisplayed / isEnabled / isClickable
  • 组合用 -> allOf / anyOf / not
  • 自己造 -> 自定义 TypeSafeMatcher(下一章开讲)

记住,匹配器只是工具,真正重要的是你理解“你要匹配什么”。把基础打扎实了,后面写自定义匹配器才会信手拈来。