Espresso 列表

Espresso 为两种类型的列表(即适配器视图和回收器视图)提供了滚动到特定项目或对特定项目执行操作的机制。

在处理列表(尤其是使用 RecyclerViewAdapterView 对象创建的列表)时,您想要查看的视图甚至可能不在屏幕上,因为当您滚动屏幕时,只会显示和回收少量的子视图。在这种情况下,不能使用 scrollTo() 方法,因为它需要一个现有的视图。

与适配器视图列表项交互

在您的搜索中先使用 onData() 方法(而不是使用 onView() 方法),并针对支持您想要匹配的视图的数据提供一个匹配器。Espresso 将完成在 Adapter 对象中查找相应行并使相应项目在视口中可见的所有工作。

使用自定义视图匹配器来匹配数据

下面的 Activity 包含一个 ListView,该视图由一个 SimpleAdapter 支持,该适配器将每一行的数据保存在一个 Map<String, Object> 对象中。

当前显示在屏幕上的列表 Activity 包含一个具有 23 个项目的列表。每个项目都有一个数字,以字符串形式存储且映射到一个不同的数字,而后者以对象形式存储。

每个映射都有两个条目:一个键 "STR",它包含一个字符串,如 "item: x";一个键 "LEN",它包含一个 Integer,该整数表示内容的长度。例如:

    {"STR" : "item: 0", "LEN": 7}
    

要点击包含“item: 50”的行,代码如下所示:

Kotlin

    onData(allOf(`is`(instanceOf(Map.class)), hasEntry(equalTo("STR"),
            `is`("item: 50")))).perform(click())
    

Java

    onData(allOf(is(instanceOf(Map.class)), hasEntry(equalTo("STR"), is("item: 50"))))
        .perform(click());
    

请注意,Espresso 会根据需要自动滚动浏览列表。

接下来,我们拆开 onData() 内的 Matcher<Object>is(instanceOf(Map.class)) 方法将搜索范围缩小到由 Map 对象支持的 AdapterView 的任何项目。

针对我们目前讨论的情况,查询的这一方面与列表视图的每一行都匹配,但我们要专门点击一个项目,因此我们通过以下代码来进一步缩小搜索范围:

Kotlin

    hasEntry(equalTo("STR"), `is`("item: 50"))
    

Java

    hasEntry(equalTo("STR"), is("item: 50"))
    

Matcher<String, Object> 将与包含一个具有键 "STR" 和值 "item: 50" 的条目的任何映射匹配。因为查询此内容的代码很长并且我们希望在其他位置重复使用该代码,所以我们来为此编写一个自定义 withItemContent 匹配器:

Kotlin

    return object : BoundedMatcher<Object, Map>(Map::class.java) {
        override fun matchesSafely(map: Map): Boolean {
            return hasEntry(equalTo("STR"), itemTextMatcher).matches(map)
        }

        override fun describeTo(description: Description) {
            description.appendText("with item content: ")
            itemTextMatcher.describeTo(description)
        }
    }
    

Java

    return new BoundedMatcher<Object, Map>(Map.class) {
        @Override
        public boolean matchesSafely(Map map) {
            return hasEntry(equalTo("STR"), itemTextMatcher).matches(map);
        }

        @Override
        public void describeTo(Description description) {
            description.appendText("with item content: ");
            itemTextMatcher.describeTo(description);
        }
    };
    

您将 BoundedMatcher 用作基匹配器,因为要只与 Map 类型的对象匹配。替换 matchesSafely() 方法,放入之前找到的匹配器,并将其对照可作为参数传递的 Matcher<String> 进行匹配。这样,您便可以调用 withItemContent(equalTo("foo"))。为了代码简便起见,您可以再创建一个已调用 equalTo() 且接受 String 对象的匹配器:

Kotlin

    fun withItemContent(expectedText: String): Matcher<Object> {
        checkNotNull(expectedText)
        return withItemContent(equalTo(expectedText))
    }
    

Java

    public static Matcher<Object> withItemContent(String expectedText) {
        checkNotNull(expectedText);
        return withItemContent(equalTo(expectedText));
    }
    

现在,要点击该项目,代码非常简单:

Kotlin

    onData(withItemContent("item: 50")).perform(click())
    

Java

    onData(withItemContent("item: 50")).perform(click());
    

如需了解此测试的完整代码,请查看 GitHub 上的 AdapterViewTest 类中的 testClickOnItem50() 方法以及这一自定义 LongListMatchers 匹配器。

匹配特定子视图

上面的示例会在 ListView 的整行中间发出一次点击。但是,如果我们要对行的特定子级执行操作,该怎么办呢?例如,我们想要点击 LongListActivity 的行的第二列,该列显示第一列中内容的 String.length:

在此示例中,仅提取一段特定内容的长度会有好处。此过程涉及确定某一行中第二列的值。

只需将 onChildView() 规范添加到您的 DataInteraction 实现即可:

Kotlin

    onData(withItemContent("item: 60"))
        .onChildView(withId(R.id.item_size))
        .perform(click())
    

Java

    onData(withItemContent("item: 60"))
        .onChildView(withId(R.id.item_size))
        .perform(click());
    

与回收器视图列表项交互

RecyclerView 对象的工作方式与 AdapterView 对象不同,因此不能使用 onData() 方法与其交互。

要使用 Espresso 与 RecyclerView 交互,您可以使用 espresso-contrib 软件包,该软件包具有 RecyclerViewActions 的集合,可用于滚动到相应位置或对项目执行操作:

  • scrollTo() - 滚动到匹配的视图。
  • scrollToHolder() - 滚动到匹配的视图持有者。
  • scrollToPosition() - 滚动到特定位置。
  • actionOnHolderItem() - 对匹配的视图持有者执行视图操作。
  • actionOnItem() - 对匹配的视图执行视图操作。
  • actionOnItemAtPosition() - 在特定位置对视图执行视图操作。

以下代码段展示了 RecyclerViewSample 中的一些示例:

Kotlin

    @Test fun scrollToItemBelowFold_checkItsText() {
        // First, scroll to the position that needs to be matched and click on it.
        onView(ViewMatchers.withId(R.id.recyclerView))
            .perform(
                RecyclerViewActions.actionOnItemAtPosition(
                    ITEM_BELOW_THE_FOLD,
                    click()
                )
            )

        // Match the text in an item below the fold and check that it's displayed.
        val itemElementText = "${activityRule.activity.resources
            .getString(R.string.item_element_text)} ${ITEM_BELOW_THE_FOLD.toString()}"
        onView(withText(itemElementText)).check(matches(isDisplayed()))
    }
    

Java

    @Test
    public void scrollToItemBelowFold_checkItsText() {
        // First, scroll to the position that needs to be matched and click on it.
        onView(ViewMatchers.withId(R.id.recyclerView))
                .perform(RecyclerViewActions.actionOnItemAtPosition(ITEM_BELOW_THE_FOLD,
                click()));

        // Match the text in an item below the fold and check that it's displayed.
        String itemElementText = activityRule.getActivity().getResources()
                .getString(R.string.item_element_text)
                + String.valueOf(ITEM_BELOW_THE_FOLD);
        onView(withText(itemElementText)).check(matches(isDisplayed()));
    }
    

Kotlin

    @Test fun itemInMiddleOfList_hasSpecialText() {
        // First, scroll to the view holder using the isInTheMiddle() matcher.
        onView(ViewMatchers.withId(R.id.recyclerView))
            .perform(RecyclerViewActions.scrollToHolder(isInTheMiddle()))

        // Check that the item has the special text.
        val middleElementText = activityRule.activity.resources
                .getString(R.string.middle)
        onView(withText(middleElementText)).check(matches(isDisplayed()))
    }
    

Java

    @Test
    public void itemInMiddleOfList_hasSpecialText() {
        // First, scroll to the view holder using the isInTheMiddle() matcher.
        onView(ViewMatchers.withId(R.id.recyclerView))
                .perform(RecyclerViewActions.scrollToHolder(isInTheMiddle()));

        // Check that the item has the special text.
        String middleElementText =
                activityRule.getActivity().getResources()
                .getString(R.string.middle);
        onView(withText(middleElementText)).check(matches(isDisplayed()));
    }
    

其他资源

如需详细了解如何在 Android 测试中使用 Espresso 列表,请参阅以下资源。

示例

  • DataAdapterSample:展示 Espresso 中适用于列表和 AdapterView 对象的 onData() 入口点。