Espresso 列表

Espresso 提供了用于滚动到特定项目或对特定项目进行操作的机制,适用于两种类型的列表:适配器视图和回收器视图。

在处理列表(尤其是使用 RecyclerViewAdapterView 对象创建的列表)时,您感兴趣的视图可能甚至不在屏幕上,因为只显示了少数子项,并在您滚动时循环使用。在这种情况下,无法使用 scrollTo() 方法,因为它需要现有的视图。

与适配器视图列表项交互

不要使用 onView() 方法,而是从 onData() 开始搜索,并针对您要匹配的视图背后的数据提供匹配器。Espresso 将完成所有工作,以在 Adapter 对象中找到该行,并使该项目在视窗中可见。

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

下面的活动包含一个 ListView,它由一个 SimpleAdapter 支持,该适配器在 Map<String, Object> 对象中保存每行的信息。

The list activity currently shown on the screen contains a list with
          23 items. Each item has a number, stored as a String, mapped to a
          different number, which is stored as an Object instead.

每个映射都包含两个条目:一个键 "STR",其中包含一个字符串,例如 "item: x",以及一个键 "LEN",其中包含一个 Integer,表示内容的长度。例如

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

单击包含 "item: 50" 的行的代码如下所示

Kotlin

onData(allOf(`is`(instanceOf(Map::class.java)), 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());

有关此测试的完整代码,请查看 AdapterViewTest 类中的 testClickOnItem50() 方法和 GitHub 上的 这个自定义 LongListMatchers 匹配器。

匹配特定子视图

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

In this example, it would be beneficial to extract just the length of
          a particular piece of content. This process involves determining the
          value of the second column in a row.

只需在您的 DataInteraction 实现中添加一个 onChildView() 规范。

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 列表项交互

RecyclerView 对象的工作方式与 AdapterView 对象不同,因此 onData() 不能用于与它们交互。

要使用 Espresso 与 RecyclerViews 交互,可以使用 espresso-contrib 包,该包包含一组可用于滚动到特定位置或对项目执行操作的 RecyclerViewActions

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

以下代码片段展示了来自 RecyclerViewSample 示例的一些示例。

Kotlin

@Test(expected = PerformException::class)
fun itemWithText_doesNotExist() {
    // Attempt to scroll to an item that contains the special text.
    onView(ViewMatchers.withId(R.id.recyclerView))
        .perform(
            // scrollTo will fail the test if no item matches.
            RecyclerViewActions.scrollTo(
                hasDescendant(withText("not in the list"))
            )
        )
}

Java

@Test(expected = PerformException.class)
public void itemWithText_doesNotExist() {
    // Attempt to scroll to an item that contains the special text.
    onView(ViewMatchers.withId(R.id.recyclerView))
            // scrollTo will fail the test if no item matches.
            .perform(RecyclerViewActions.scrollTo(
                    hasDescendant(withText("not in the list"))
            ));
}

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 的 onData() 入口点,适用于列表和 AdapterView 对象。