本文档介绍如何使用 Espresso API 完成常见的自动化测试任务。
使用 Espresso API 时,我们建议测试创建者从用户与应用交互时可能会执行哪些操作(即,找到界面元素并与之交互)的角度进行思考。同时,该框架阻止直接访问应用的 Activity 和视图,因为保留这些对象并在界面线程外对其执行操作是导致测试不稳定的主要根源。因此,您不会在 Espresso API 中看到诸如 getView()
和 getCurrentActivity()
之类的方法。不过,您仍然可以安全地对视图执行操作,只需实现您自己的 ViewAction
和 ViewAssertion
子类即可。
API 组件
Espresso 的主要组件包括:
- Espresso - 用于与视图交互(通过
onView()
和onData()
)的入口点。此外,还公开不一定与任何视图相关联的 API,如pressBack()
。 - ViewMatchers - 实现
Matcher<? super View>
接口的对象的集合。您可以将其中一个或多个对象传递给onView()
方法,以在当前视图层次结构中找到某个视图。 - ViewActions - 可以传递给
ViewInteraction.perform()
方法的ViewAction
对象的集合,例如click()
。 - ViewAssertions - 可以通过
ViewInteraction.check()
方法传递的ViewAssertion
对象的集合。在大多数情况下,您将使用 matches 断言,它使用视图匹配器断言当前选定视图的状态。
示例:
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()
缩小搜索范围,考虑将其视为无障碍功能错误。 - 使用描述内容最少的匹配器找到您要查找的一个视图。不要过度指定,因为这样会强制框架执行不必要的工作。例如,如果某个视图可由其文本唯一标识,则您无需指定该视图也可从
TextView
分配。对于许多视图来说,指定视图的R.id
应该就足够了。 - 如果目标视图在
AdapterView
内(如ListView
、GridView
或Spinner
),则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
的自定义实现违反继承约定,那么在使用 onData()
方法(尤其是 getItem()
API)时可能会出现问题。在这种情况下,最好的做法是重构应用代码。如果您无法执行此操作,则可以实现匹配的自定义 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 中的 Hierarchy Viewer 进行解释说明总是很有帮助。
适配器视图警告
Espresso 会向用户发出有关存在 AdapterView
微件的警告。当 onView()
操作抛出 NoMatchingViewException
并且视图层次结构中存在 AdapterView
微件时,最常见的解决方案是使用 onData()
。异常消息将包含一条警告,其中列出了相应的适配器视图。您可以使用此信息来调用 onData()
以加载目标视图。
其他资源
如需详细了解如何在 Android 测试中使用 Espresso,请参阅以下资源。
示例
- CustomMatcherSample:展示如何扩展 Espresso 以与
EditText
对象的 hint 属性匹配。 - RecyclerViewSample:Espresso 的
RecyclerView
操作。 - (更多内容)