Espresso 基础知识

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

Espresso API 鼓励测试编写者从用户与应用程序交互时可能执行的操作(查找 UI 元素并与之交互)的角度进行思考。 同时,该框架阻止直接访问应用程序的活动和视图,因为在 UI 线程之外保留这些对象并对其进行操作是导致测试不稳定的主要原因。 因此,您不会在 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 对象集合。大多数情况下,您将使用 matches 断言,它使用 View 匹配器来断言当前选定视图的状态。

示例

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.idR.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()来缩小搜索范围,请将其视为一个可访问性错误。
  • 使用找到您要查找的唯一视图的描述性最少的匹配器。不要过度指定,因为这会强制框架执行比必要更多的工作。例如,如果一个视图可以通过其文本唯一识别,则无需指定该视图还可以从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是一种特殊的窗口小部件,它会从适配器动态加载其数据。AdapterView最常见的示例是ListView。与LinearLayout等静态窗口小部件相反,只有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****

在处理复杂的视图层次结构或窗口小部件的意外行为时,始终可以使用Android Studio中的层次结构查看器进行解释。

适配器视图警告

Espresso会警告用户AdapterView窗口小部件的存在。当onView()操作抛出NoMatchingViewException并且视图层次结构中存在AdapterView窗口小部件时,最常见的解决方案是使用onData()。异常消息将包含一条警告,其中包含适配器视图的列表。您可以使用此信息调用onData()来加载目标视图。

其他资源

有关在Android测试中使用Espresso的更多信息,请参阅以下资源。

示例