Espresso 基础知识

本文档介绍了如何使用 Espresso API 完成常见的自动化测试任务。

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

API 组件

Espresso 的主要组件包括:

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

示例:

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"))));

如需了解 Espresso 提供的视图匹配器,请参阅 ViewMatchers

注意事项

  • 在运行状况良好的应用中,用户可以与之互动的所有视图都应包含描述性文本或具有内容说明。如需了解详情,请参阅改进应用的无障碍功能。如果您无法使用 withText()withContentDescription() 缩小搜索范围,请考虑将其视为无障碍功能 bug。
  • 使用描述最少的匹配器查找您要找的一个视图。不要过度指定,因为这会强制框架执行不必要的工作。例如,如果某个视图可由其文本唯一标识,则您无需指定该视图也可从 TextView 分配。对于许多视图来说,指定视图的 R.id 应该就足够了。
  • 如果目标视图在 AdapterView(例如 ListViewGridViewSpinner)内,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());

如需了解 Espresso 提供的视图操作,请参阅 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 的自定义实现违反继承协定(尤其是 getItem() API),那么在使用 onData() 方法时可能会出现问题。在这种情况下,最好的做法是重构应用代码。如果您无法执行此操作,可以实现匹配的自定义 AdapterViewProtocol。如需了解详情,请查看 Espresso 提供的默认 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,请参阅以下资源。

示例