本文档介绍了如何使用 Espresso API 完成常见的自动化测试任务。
Espresso API 鼓励测试编写者从用户与应用程序交互的角度(定位界面元素并与之交互)来思考测试。同时,该框架禁止直接访问应用程序的 Activity 和 View,因为持有这些对象并在 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 断言,它利用 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.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 以了解 Espresso 提供的视图匹配器。
注意事项
- 在行为良好的应用程序中,用户可交互的所有视图都应包含描述性文本或具有内容描述。详情请参阅让应用更易于访问。如果你无法使用
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 以了解 Espresso 提供的视图操作。
检查视图断言
可以使用 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 是一种特殊类型的控件,它从 Adapter 动态加载数据。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操作。 - (更多...)