本文档将介绍如何使用 Espresso API。
Espresso API 鼓励测试作者从用户的角度思考问题
在与应用交互时执行的操作 - 定位界面元素并与之交互
。同时,该框架会阻止对 activity 的直接访问
和视图,因为保留这些对象并操作
在界面线程之外对它们进行构建是导致测试不稳定的主要原因。因此,您需要
在 Espresso API 中看不到 getView()
和 getCurrentActivity()
等方法。
您仍然可以安全地对视图执行操作,只需实现自己的
ViewAction
和ViewAssertion
。
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
),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());
请参阅 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,请查阅 以下资源。
示例
- CustomMatcherSample:
介绍如何扩展 Espresso 以与
EditText
对象的 hint 属性匹配。 - RecyclerViewSample:
适用于 Espresso 的
RecyclerView
操作。 - (更多…)