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,总结出几条原则:
- 优先用
withId。它是性能最好、唯一性最强的匹配器。只要你的 UI 组件有 id,就别用其他匹配器绕弯子。 withText适合确认内容,不适合定位。尤其是多语言 App,文本翻译一变你的测试就挂了。所以拿withText做检查(check(matches(...)))比拿它定位更靠谱。- 善用
anyOf做兼容。比如同个功能在不同 Android 版本上有不同实现(系统弹窗按钮文本不同),anyOf就能优雅地兼容。 - 避免模糊匹配。用
withText(containsString("登"))可能同时命中“登录”和“登出”两个按钮——嗯,我亲眼见过同事因为这个调试了一下午。
我曾经在测试 RecyclerView 时,想用
withId(R.id.item_title) 获取每一项里的 TextView,结果发现同一个 id 出现了很多次(item 被复用)。withId 本身不保证唯一性,它只是匹配条件。这时候必须配合 inAdapterView 或 atPosition 来缩小范围。
4.5 匹配器结构总览
把上面的内容画成一张图,你会看得更清楚。ViewMatchers 体系大致如下:
4.6 快速记忆口诀
最后分享一个我自己的记忆方法,方便你脑子里快速检索:
- 找特征 ->
withId/withText/withTag - 查状态 ->
isDisplayed/isEnabled/isClickable - 组合用 ->
allOf/anyOf/not - 自己造 -> 自定义
TypeSafeMatcher(下一章开讲)
记住,匹配器只是工具,真正重要的是你理解“你要匹配什么”。把基础打扎实了,后面写自定义匹配器才会信手拈来。