Espresso 基础知识

本文档将介绍如何使用 Espresso API。

Espresso API 鼓励测试作者从用户的角度思考问题 在与应用交互时执行的操作 - 定位界面元素并与之交互 。同时,该框架会阻止对 activity 的直接访问 和视图,因为保留这些对象并操作 在界面线程之外对它们进行构建是导致测试不稳定的主要原因。因此,您需要 在 Espresso API 中看不到 getView()getCurrentActivity() 等方法。 您仍然可以安全地对视图执行操作,只需实现自己的 ViewActionViewAssertion

API 组件

Espresso 的主要组件包括:

  • Espresso - 用于与视图交互(通过 onView()onData())。此外,还公开不一定与任何视图相关联的 API,例如 为 pressBack()
  • ViewMatchers - 实现 Matcher<? super View> 接口。您可以将其中一个或多个对象传递给 onView() 方法,用于在当前视图层次结构中查找视图。
  • ViewActions - 可传递到的 ViewAction 对象的集合 ViewInteraction.perform() 方法,例如 click()
  • ViewAssertions - ViewAssertion 对象的集合, 传递了 ViewInteraction.check() 方法。大多数情况下,您会使用 与断言匹配,该断言使用视图匹配器断言 当前选定的数据视图

示例:

Kotlin

// withId(R.id.my_view) is a ViewMatcher
// click() is a ViewAction
// matches(isDisplayed()) is a ViewAssertion
onView(withId(R.id.my_view))
    .perform(click())
    .check(matches(isDisplayed()))

Java

// withId(R.id.my_view) is a ViewMatcher
// click() is a ViewAction
// matches(isDisplayed()) is a ViewAssertion
onView(withId(R.id.my_view))
    .perform(click())
    .check(matches(isDisplayed()));

查找视图

在绝大多数情况下,onView() 方法会使用 hamcrest 匹配器 与当前数据视图中的一个(且只有一个)数据视图匹配 层级结构。匹配器功能强大,使用过 以及 Mockito 或 JUnit。如果您不熟悉 hamcrest 匹配器, 建议您先快速浏览此 演示文稿

所需视图通常具有唯一的 R.id,并且简单的 withId 匹配器将 缩小视图搜索范围不过,在很多合理的情况下 在测试开发时无法确定 R.id。例如,特定数据视图 可能不包含 R.id,或者 R.id 不具有唯一性。这可能会使 插桩测试脆弱且编写复杂,因为 使用 findViewById() 访问视图不起作用。因此,您可以 需要访问持有视图的 activity 或 fragment 的私有成员,或 找到具有已知 R.id 的容器,并导航到该容器的内容, 特定视图

Espresso 允许您缩小视图,从而彻底解决了此问题 使用现有的 ViewMatcher 对象或您自己的自定义对象。

R.id 查找视图就像调用 onView() 一样简单:

Kotlin

onView(withId(R.id.my_view))

Java

onView(withId(R.id.my_view));

有时,会在多个视图之间共享 R.id 值。出现这种情况时 尝试使用特定的 R.id 会抛出异常,例如 AmbiguousViewMatcherException。异常消息中会 当前视图层次结构的图示,您可以搜索和查找 与非唯一 R.id 匹配的视图:

java.lang.RuntimeException:
androidx.test.espresso.AmbiguousViewMatcherException
This matcher matches multiple views in the hierarchy: (withId: is <123456789>)

...

+----->SomeView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=false, enabled=true,
selected=false, is-layout-requested=false, text=,
root-is-layout-requested=false, x=0.0, y=625.0, child-count=1}
****MATCHES****
|
+------>OtherView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=true, enabled=true,
selected=false, is-layout-requested=false, text=Hello!,
root-is-layout-requested=false, x=0.0, y=0.0, child-count=1}
****MATCHES****

仔细浏览数据视图的各种属性后,您可能会发现 可识别的媒体资源在上面的示例中,其中一个视图包含 "Hello!"。您可以利用此功能,使用组合键来缩小搜索范围 匹配器:

Kotlin

onView(allOf(withId(R.id.my_view), withText("Hello!")))

Java

onView(allOf(withId(R.id.my_view), withText("Hello!")));

您也可以选择不反转任何匹配器:

Kotlin

onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))))

Java

onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))));

请参阅 ViewMatchers

注意事项

  • 在运行良好的应用中,用户可以与之互动的所有视图 应包含描述性文字或有内容说明。请参阅 让应用使用起来更没有障碍 。如果您无法使用 withText()withContentDescription(),请考虑将其视为无障碍功能 bug。
  • 使用最不具描述性的匹配器查找所需的一个视图 。不要过度指定,因为这会迫使框架完成比 。例如,如果某个视图可通过其文本唯一标识, 而无需指定该视图也可从 TextView 分配。对于 视图,则视图的 R.id 应该就足够了。
  • 如果目标视图在 AdapterView 内(例如 ListView), GridViewSpinner - onView() 方法可能不起作用。在这些 则应改用 onData()

对视图执行操作

找到适合目标视图的匹配器后,可以 使用 perform 方法对实例执行 ViewAction 实例。

例如,要点击视图,请编写以下代码:

Kotlin

onView(...).perform(click())

Java

onView(...).perform(click());

您可以通过一次 perform 调用来执行多项操作:

Kotlin

onView(...).perform(typeText("Hello"), click())

Java

onView(...).perform(typeText("Hello"), click());

如果您要使用的视图位于 ScrollView(垂直或 水平),请考虑之前需要显示视图的操作 (如 click()typeText())与 scrollTo() 一起使用。这个 确保在继续其他操作之前显示视图:

Kotlin

onView(...).perform(scrollTo(), click())

Java

onView(...).perform(scrollTo(), click());

请参阅 ViewActions

检查视图断言

您可以使用 check() 将断言应用于当前选定的视图 方法。最常用的断言是 matches() 断言。它使用 ViewMatcher 对象断言当前所选视图的状态。

例如,如需检查视图是否包含文本 "Hello!",请编写以下代码:

Kotlin

onView(...).check(matches(withText("Hello!")))

Java

onView(...).check(matches(withText("Hello!")));

如果您要断言 "Hello!" 是视图的内容,则编写以下代码的做法不妥:

Kotlin

// Don't use assertions like withText inside onView.
onView(allOf(withId(...), withText("Hello!"))).check(matches(isDisplayed()))

Java

// Don't use assertions like withText inside onView.
onView(allOf(withId(...), withText("Hello!"))).check(matches(isDisplayed()));

另一方面,如果您想断言包含文本 "Hello!" 的视图 例如,在更改视图可见性标志后, 就可以了

视图断言简单测试

在此示例中,SimpleActivity 包含一个 Button 和一个 TextView。当 按钮时,TextView 的内容会变为 "Hello Espresso!"

使用 Espresso 进行测试的方法如下:

点击按钮

第一步是查找有助于找到按钮的属性。通过 像预期的那样,SimpleActivity 中的按钮具有唯一的 R.id

Kotlin

onView(withId(R.id.button_simple))

Java

onView(withId(R.id.button_simple));

现在执行点击:

Kotlin

onView(withId(R.id.button_simple)).perform(click())

Java

onView(withId(R.id.button_simple)).perform(click());

验证 TextView 文本

包含要验证的文本的 TextView 也具有唯一的 R.id

Kotlin

onView(withId(R.id.text_simple))

Java

onView(withId(R.id.text_simple));

现在验证内容文本:

Kotlin

onView(withId(R.id.text_simple)).check(matches(withText("Hello Espresso!")))

Java

onView(withId(R.id.text_simple)).check(matches(withText("Hello Espresso!")));

检查适配器视图中的数据加载

AdapterView 是一种特殊类型的 widget,可从 适配器最常见的 AdapterView 示例是 ListView。如 与 LinearLayout 等静态 widget 不同, 可将 AdapterView 个子视图加载到当前视图层次结构中。简单的 onView()搜索找不到当前未加载的视图。

Espresso 处理此问题的方法是提供一个单独的 onData() 入口点,该入口点 能够先加载相关适配器项,并在调用前将焦点置于其上。 对其或其任何子项执行操作。

警告AdapterView在使用onData()时可能会遇到问题 方法,特别是 getItem() API。在这种情况下,最佳做法是 重构应用代码。如果无法做到这一点,您可以 与自定义 AdapterViewProtocol 匹配。有关详情,请 查看默认的 <ph type="x-smartling-placeholder"></ph> AdapterViewProtocols 类。

适配器视图简单测试

这个简单的测试演示了如何使用 onData()SimpleActivity 包含 Spinner,其中包含几个代表咖啡饮料类型的项。当 项被选中时,TextView 会变为 "One %s a day!",其中 %s 表示选定的项。

此测试的目的是打开 Spinner,选择特定内容,然后 验证 TextView 是否包含该项。由于 Spinner 类基于 在 AdapterView 上,对于以下实例,建议使用 onData()(而非 onView()) 匹配商品。

打开项目选择视图

Kotlin

onView(withId(R.id.spinner_simple)).perform(click())

Java

onView(withId(R.id.spinner_simple)).perform(click());

选择项目

对于项选择,Spinner 会使用其内容创建一个 ListView。 此视图可能会非常长,并且相应元素可能不会添加到视图中 层级结构。通过使用 onData(),我们可以强制将所需元素放到视图中 层级结构。Spinner 中的项是字符串,因此我们希望匹配项 (等于字符串 "Americano"):

Kotlin

onData(allOf(`is`(instanceOf(String::class.java)),
        `is`("Americano"))).perform(click())

Java

onData(allOf(is(instanceOf(String.class)), is("Americano"))).perform(click());

验证文本是否正确

Kotlin

onView(withId(R.id.spinnertext_simple))
    .check(matches(withText(containsString("Americano"))))

Java

onView(withId(R.id.spinnertext_simple))
    .check(matches(withText(containsString("Americano"))));

调试

当测试失败时,Espresso 会提供有用的调试信息。

日志记录

Espresso 会将所有视图操作记录到 logcat。例如:

ViewInteraction: Performing 'single click' action on view with text: Espresso

视图层次结构

onView() 时,Espresso 会在异常消息中输出视图层次结构 失败。

  • 如果 onView() 找不到目标视图,则 NoMatchingViewException 。您可以检查异常字符串中的视图层次结构以进行分析 匹配器与任何视图都不匹配的原因。
  • 如果 onView() 找到了多个与给定匹配器匹配的视图,则会返回一个 抛出 AmbiguousViewMatcherException。输出视图层次结构 匹配的视图使用 MATCHES 标签进行标记:
java.lang.RuntimeException:
androidx.test.espresso.AmbiguousViewMatcherException
This matcher matches multiple views in the hierarchy: (withId: is <123456789>)

...

+----->SomeView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=false, enabled=true,
selected=false, is-layout-requested=false, text=,
root-is-layout-requested=false, x=0.0, y=625.0, child-count=1}
****MATCHES****
|
+------>OtherView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=true, enabled=true,
selected=false, is-layout-requested=false, text=Hello!,
root-is-layout-requested=false, x=0.0, y=0.0, child-count=1}
****MATCHES****

处理复杂的视图层次结构或 widget 的意外行为时 最好使用 Android Studio 中的 Hierarchy Viewer, 进行说明

适配器视图警告

Espresso 会向用户发出有关存在 AdapterView 微件的警告。当 onView() 操作会抛出 NoMatchingViewException,并且 AdapterView widget ,最常见的解决方案是使用 onData()。 异常消息将包含一条警告,其中列出了相应的适配器视图。 您可以使用此信息来调用 onData() 以加载目标视图。

其他资源

如需详细了解如何在 Android 测试中使用 Espresso,请查阅 以下资源。

示例