第三章节:onView 与 onData 的使用归纳
这一章,我们聊聊 Espresso 里两个最基础但最容易搞混的入口——onView 和 onData。很多刚接触 Espresso 的朋友上来就写 onView(withId(R.id.my_list)),结果跑测试时要么找不到视图,要么抛异常。为什么会这样?说白了,它们俩在底层走的完全不是一条路。
它们分别解决什么问题?
onView 针对的是 已经渲染在视图树中的 View。你调用 perform() 或 check() 时,它会通过 ViewMatcher 在当前窗口的视图层级里搜索匹配的那个 View。Button、TextView、RecyclerView 的 item 里面某个子 View 都要用 onView。
onData 专门用来处理 AdapterView 里的数据项。ListView、GridView、Spinner、AutoCompleteTextView 这些控件,它的 item 是动态通过 Adapter 创建的,很多 item 可能根本没被显示在屏幕上。直接 onView 拿不到那些离屏 item,但 onData 能模拟滚动、找到与数据条件匹配的行,再交给 ViewInteraction 去操作。
我记得刚做第一个项目时,同事对着一个 Spinner 写了 onView(withText("选项三")),死活找不到。我一看代码,Spinner 的 item 是 SimpleAdapter 填充的,每个 item 的 TextView 不属于当前视图树的直接子 View。换成 onData(allOf(is(instanceOf(String.class)), is("选项三"))) 就对了。
addView() 添加到父容器里的,用 onView;如果是通过 Adapter 的 getView() 创建的,用 onData。
核心区别对比表
| 维度 | onView | onData |
|---|---|---|
| 适用控件 | 普通 View / ViewGroup | AdapterView(ListView、GridView、Spinner) |
| 匹配目标 | 视图对象本身 | Adapter 中的底层数据对象 |
| 是否需要 item 在屏幕上 | 需要 | 不需要(自动滚动) |
| 返回类型 | ViewInteraction | DataInteraction |
| 常见匹配器 | withId, withText, withTag 等 | onData(Matcher<Object>),可配合 allOf, instanceOf |
| 性能 | 快,直接树搜索 | 稍慢,需要遍历 Adapter |
onView 找的是 UI 上的“壳”,onData 找的是数据里的“核”。
onData 的使用流程
咱们写一个实际案例。假设有个 ListView,每个 item 展示一个 String。我想点中值为“Espresso”的那一项:
onData(allOf(is(instanceOf(String.class)), is("Espresso")))
.inAdapterView(withId(R.id.my_listview))
.perform(click());
这里有三步:
- 数据匹配:
is(instanceOf(String.class))保证匹配的是字符串类型;is("Espresso")精确匹配值。 - 定位 AdapterView:
.inAdapterView(withId(R.id.my_listview))告诉 Espresso 在哪个 AdapterView 里找。 - 交互:
.perform(click())对匹配到的 item 执行点击。
如果 item 包含多个数据字段(比如自定义对象),可以用 onData(hasEntry(equalTo("name"), equalTo("Espresso")))。我曾经在测试一个联系人列表时,需要点中姓“王”的第一个联系人,用 onData(hasEntry(equalTo("surname"), equalTo("王"))) 轻松搞定。注意,hasEntry 是 Hamcrest 的标准匹配器,不是 Espresso 自带的,需要依赖 hamcrest-library。
onView 也能处理 AdapterView?
有朋友可能会问:如果我已经通过手动滚动把某个 item 显示到屏幕上了,能不能直接用 onView 去点?理论上可以,但极不推荐。理由有三:
- 你无法保证 item 一定在屏幕上(不同设备、不同分辨率)。
- Espresso 的
onData内部已经处理好了 IdlingResource 同步,省去了手动等待的麻烦。 - 测试代码的可维护性差。改一个 item 间距可能就让你的硬编码坐标失效。
onData 对 RecyclerView 无效。但你可以用 RecyclerViewActions 或者写自定义匹配器。这一点我后面专门讲。
什么时候用哪个?一张图帮你理清思路
这张图是我平时给组里新人培训时用的。你从上往下走:先判断你的 View 是不是通过 Adapter 创建的?如果是,走右边 onData 路线——匹配数据对象、定位 AdapterView、然后交互;如果不是,走左边 onView 路线——直接写 ViewMatcher 然后交互。注意最底部的补充:RecyclerView 虽然也用 Adapter,但它不是 AdapterView,需要专用解决方案。
避坑指南:我曾经踩过的三个雷
- 适配器没数据:
onData匹配时如果 Adapter 返回 null 或者数据列表为空,会抛出PerformException。我建议在测试前先用onView(withId(...)).check(matches(isDisplayed()))确保 AdapterView 已经可见,再调onData。 - 匹配器写得太宽松:直接
onData(is(anything()))会匹配到第一个 item,但假如数据集有重复数据,Espresso 会报“AmbiguousViewMatcherException”。尽量用allOf缩小范围。 - RecyclerView 用 onData:早期的 Espresso 确实不支持,现在我还没见过有人在 RecyclerView 上用
onData。如果你发现你的 App 里用了 RecyclerView,记住用onView(withRecyclerView(R.id.recycler).atPosition(2))或者自定义RecyclerViewActions。
System.out.println(targetView.getClass().getName()) 打印它的类型。看到 android.widget.ListView、android.widget.Spinner 这类就果断用 onData。
总结一下核心原则
我个人习惯把 onView 和 onData 看成两种不同的“搜索算法”。onView 是在视图树的深度优先遍历中找,onData 是遍历 Adapter 的数据集合。选择哪一个,看你的测试对象是“UI 壳”还是“数据核”。只要记住上面那张图,90% 的场景都不会选错。