Espresso リスト

Espresso には、アダプター ビューとリサイクラー ビューの 2 種類のリストについて、特定の項目までスクロールまたは操作するためのメカニズムが用意されています。

リスト(特に RecyclerView または AdapterView オブジェクトで作成されたリスト)を処理する場合、目的のビューが画面に表示されないことがあります。これは、スクロールすると少数の子しか表示されず、リサイクルされるためです。この場合、既存のビューを必要とするため、scrollTo() メソッドを使用できません。

アダプター ビューのリスト項目の操作

onView() メソッドを使用する代わりに、onData() で検索を開始し、マッチングするビューの背後にあるデータに対するマッチャーを用意します。Espresso は、Adapter オブジェクト内の行を見つけてビューポートにアイテムを表示するためのすべての作業を行います。

カスタムビュー マッチャーを使用したデータのマッチング

次のアクティビティには ListView が含まれており、これは Map<String, Object> オブジェクト内の各行のデータを保持する SimpleAdapter を基盤としています。

現在画面に表示されているリスト アクティビティには、23 項目のリストが含まれています。各項目には数値が含まれ、文字列として保存されます。数値は別の数値にマッピングされ、代わりにオブジェクトとして保存されます。

各マップには 2 つのエントリがあります。1 つは "item: x" などの String を含むキー "STR" で、もう 1 つはコンテンツの長さを表す Integer を含むキー "LEN" です。次に例を示します。

{"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" のエントリを含むすべての Map と一致します。この検索用のコードは長く、他の場所で再利用したいので、そのためのカスタム 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);
    }
};

Map タイプのオブジェクトのみを照合するため、BoundedMatcher をベースとして使用します。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 の行の 2 番目の列をクリックすると、最初の列にコンテンツの String.length が表示されます。

この例では、特定のコンテンツの長さだけを抽出すると便利です。このプロセスでは、行の 2 番目の列の値を決定します。

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 オブジェクトは AdapterView オブジェクトとは動作が異なるため、onData() を使用して操作することはできません。

Espresso を使用して RecyclerView を操作するには、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 でリストと AdapterView オブジェクトに対応する onData() エントリ ポイントを示します。