4、Espresso框架入门:核心概念与实战三件套
好,咱们进入正题。Espresso 是什么?说白了,它就是 Google 官方推出的 Android UI 自动化测试框架。我当年刚接触它的时候,第一反应是——这玩意儿写测试居然能这么直观?不用 sleep,不用等,代码还特别简洁。
今天这一章,我会带你彻底搞懂 Espresso 的四个核心概念:onView 与 onData、匹配器(Matcher)、操作(ViewAction)、断言(ViewAssertion)。掌握了这些,你就能写出稳定、高效的 UI 测试用例。
4.1 Espresso 的核心思想
Espresso 的设计哲学很简单:模拟用户操作,验证界面反馈。它自动处理了线程同步,你不需要手动加延迟。我个人觉得,这是它比老一代框架强的地方。
它的工作流程可以概括为三步:
- 找到控件 —— 用 onView 或 onData
- 执行操作 —— 用 ViewAction,比如点击、输入
- 验证结果 —— 用 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() 等等。
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 会自动滚动列表,找到匹配的数据项。
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)。这时候 allOf 和 hasSibling 就派上用场了。
// 找到 "密码" 标签旁边的 EditText
onView(allOf(
withId(R.id.edit_text),
hasSibling(withText("密码"))
)).perform(typeText("123456"));
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,按顺序执行。我个人习惯把输入和键盘关闭写在一起,避免键盘遮挡后续的点击操作。
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 方式,这个测试得写多长?而且还不稳定。
下一章,我会带你深入 IdlingResource,解决异步操作和网络请求的同步问题。这可是 Espresso 进阶的关键,别错过。