本文档介绍了如何使用 Espresso API 完成常见的自动化测试任务。
Espresso API 鼓励测试作者以用户在与应用程序交互时的行为方式进行思考——查找 UI 元素并与它们交互。同时,该框架阻止直接访问应用程序的活动和视图,因为在 UI 线程之外保留并操作这些对象是测试不稳定性的主要来源。因此,您不会在 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
违反了继承契约,特别是 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 中的 Hierarchy Viewer 进行说明。
适配器视图警告
Espresso 会提醒用户 AdapterView
部件的存在。当 onView()
操作抛出 NoMatchingViewException
且 AdapterView
部件存在于视图层次结构中时,最常见的解决方案是使用 onData()
。异常消息将包含一个包含适配器视图列表的警告。您可以使用此信息来调用 onData()
以加载目标视图。
其他资源
有关在 Android 测试中使用 Espresso 的更多信息,请参阅以下资源。
示例
- CustomMatcherSample: 演示如何扩展 Espresso 以匹配
EditText
对象的提示属性。 - RecyclerViewSample: Espresso 的
RecyclerView
操作。 - (更多...)