Espresso のレシピ

このドキュメントでは、各種の一般的な Espresso テストのセットアップ方法について説明します。

隣あったビューによるビューのマッチング

レイアウトには、同一のビューを複数含めることができます。たとえば、連絡先の表にある繰り返し通話ボタンに、ビュー階層内の他の通話ボタンと同じ R.id、同じテキスト、同じプロパティを設定できます。

たとえば、このアクティビティでは、テキスト "7" を含むビューが複数の行にわたって繰り返されています。

3 つのアイテムのリスト内に、同じビュー要素の 3 つのコピーを表示するリスト アクティビティ

多くの場合、一意ではないビューは、その隣にある一意のラベル(通話ボタンの隣にある連絡先の名前など)とペアになります。この場合は、hasSibling() マッチャーを使用して選択範囲を絞り込むことができます。

Kotlin

onView(allOf(withText("7"), hasSibling(withText("item: 0"))))
    .perform(click())

Java

onView(allOf(withText("7"), hasSibling(withText("item: 0"))))
    .perform(click());

アクションバー内のビューのマッチング

ActionBarTestActivity には、通常のアクションバーと、オプション メニューから作成されるコンテキスト アクションバーの 2 種類のアクションバーがあります。どちらのアクションバーにも、常に表示される 1 つのアイテムと、オーバーフロー メニューにのみ表示される 2 つのアイテムがあります。アイテムがクリックされると、TextView がクリックされたアイテムの内容に変わります。

両方のアクションバーに表示されるアイコンを一致させるのは簡単です。次のコード スニペットをご覧ください。

Kotlin

fun testClickActionBarItem() {
    // We make sure the contextual action bar is hidden.
    onView(withId(R.id.hide_contextual_action_bar))
        .perform(click())

    // Click on the icon - we can find it by the r.Id.
    onView(withId(R.id.action_save))
        .perform(click())

    // Verify that we have really clicked on the icon
    // by checking the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("Save")))
}

Java

public void testClickActionBarItem() {
    // We make sure the contextual action bar is hidden.
    onView(withId(R.id.hide_contextual_action_bar))
        .perform(click());

    // Click on the icon - we can find it by the r.Id.
    onView(withId(R.id.action_save))
        .perform(click());

    // Verify that we have really clicked on the icon
    // by checking the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("Save")));
}

アクティビティの最上部のアクションバーに保存ボタンがあります

コンテキスト アクションバー用のコードも同様で、次のようになります。

Kotlin

fun testClickActionModeItem() {
    // Make sure we show the contextual action bar.
    onView(withId(R.id.show_contextual_action_bar))
        .perform(click())

    // Click on the icon.
    onView((withId(R.id.action_lock)))
        .perform(click())

    // Verify that we have really clicked on the icon
    // by checking the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("Lock")))
}

Java

public void testClickActionModeItem() {
    // Make sure we show the contextual action bar.
    onView(withId(R.id.show_contextual_action_bar))
        .perform(click());

    // Click on the icon.
    onView((withId(R.id.action_lock)))
        .perform(click());

    // Verify that we have really clicked on the icon
    // by checking the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("Lock")));
}

アクティビティの最上部のアクションバーに南京錠ボタンがあります

通常のアクションバーでは、オーバーフロー メニューの項目のクリック操作が少し複雑になります。一部のデバイスには、ハードウェア オーバーフロー メニュー ボタン(オプション メニューにオーバーフロー メニューを開くボタン)があるためです。また、ソフトウェア オーバーフロー メニュー ボタンがあり、それによって通常のオーバーフロー メニューが開くデバイスもあります。幸いなことに、Espresso はそうした処理を行います。

通常のアクションバーの場合は、次のようにします。

Kotlin

fun testActionBarOverflow() {
    // Make sure we hide the contextual action bar.
    onView(withId(R.id.hide_contextual_action_bar))
        .perform(click())

    // Open the options menu OR open the overflow menu, depending on whether
    // the device has a hardware or software overflow menu button.
    openActionBarOverflowOrOptionsMenu(
            ApplicationProvider.getApplicationContext<Context>())

    // Click the item.
    onView(withText("World"))
        .perform(click())

    // Verify that we have really clicked on the icon by checking
    // the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("World")))
}

Java

public void testActionBarOverflow() {
    // Make sure we hide the contextual action bar.
    onView(withId(R.id.hide_contextual_action_bar))
        .perform(click());

    // Open the options menu OR open the overflow menu, depending on whether
    // the device has a hardware or software overflow menu button.
    openActionBarOverflowOrOptionsMenu(
            ApplicationProvider.getApplicationContext());

    // Click the item.
    onView(withText("World"))
        .perform(click());

    // Verify that we have really clicked on the icon by checking
    // the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("World")));
}

オーバーフロー メニューボタンが表示され、画面上部のアクションバーの下にリストが表示されている

次の図は、ハードウェア オーバーフロー メニューボタンを持つデバイスでの表示を示しています。

オーバーフロー メニューボタンはなく、画面の下部にリストが表示される

コンテキスト アクションバーの場合も、次のように簡単に処理できます。

Kotlin

fun testActionModeOverflow() {
    // Show the contextual action bar.
    onView(withId(R.id.show_contextual_action_bar))
        .perform(click())

    // Open the overflow menu from contextual action mode.
    openContextualActionModeOverflowMenu()

    // Click on the item.
    onView(withText("Key"))
        .perform(click())

    // Verify that we have really clicked on the icon by
    // checking the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("Key")))
    }
}

Java

public void testActionModeOverflow() {
    // Show the contextual action bar.
    onView(withId(R.id.show_contextual_action_bar))
        .perform(click());

    // Open the overflow menu from contextual action mode.
    openContextualActionModeOverflowMenu();

    // Click on the item.
    onView(withText("Key"))
        .perform(click());

    // Verify that we have really clicked on the icon by
    // checking the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("Key")));
    }
}

オーバーフロー メニューボタンがアクションバーに表示され、オプションのリストが画面上部のアクションバーの下に表示されます。

これらのサンプルの完全なコードを確認するには、GitHub の ActionBarTest.java サンプルをご覧ください。

ビューが表示されない場合のアサーション

一連のアクションを実行した後で、テスト対象の UI の状態をアサートすることをおすすめします。何も起こっていない場合など、ネガティブなケースになることもあります。ViewAssertions.matches() を使用すると、任意の Hamcrest ビュー マッチャーを ViewAssertion に変換できます。

以下の例では、isDisplayed() マッチャーを使用し、標準の not() マッチャーを使用して逆にしています。

Kotlin

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import org.hamcrest.Matchers.not

onView(withId(R.id.bottom_left))
    .check(matches(not(isDisplayed())))

Java

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static org.hamcrest.Matchers.not;

onView(withId(R.id.bottom_left))
    .check(matches(not(isDisplayed())));

ビューがビュー階層に存在している限り、この方法でうまくいきます。そうでない場合は NoMatchingViewException が発生するため、ViewAssertions.doesNotExist() を使用する必要があります。

ビューが存在しない場合のアサーション

ビューがビュー階層から離れた場合(アクションによって別のアクティビティへの遷移が発生した場合など)は、ViewAssertions.doesNotExist() を使用する必要があります。

Kotlin

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.matcher.ViewMatchers.withId

onView(withId(R.id.bottom_left))
    .check(doesNotExist())

Java

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
import static androidx.test.espresso.matcher.ViewMatchers.withId;

onView(withId(R.id.bottom_left))
    .check(doesNotExist());

アダプターにデータ項目がない場合のアサーション

特定のデータ項目が AdapterView 内にないことを証明するには、少し異なる処理を行う必要があります。対象の AdapterView を見つけて、それが保持しているデータを調べる必要があります。onData() を使用する必要はありません。代わりに、onView() を使用して AdapterView を見つけ、別のマッチャーを使用してビュー内のデータを処理しています。

まず、次のようにしてマッチャーを準備します。

Kotlin

private fun withAdaptedData(dataMatcher: Matcher<Any>): Matcher<View> {
    return object : TypeSafeMatcher<View>() {

        override fun describeTo(description: Description) {
            description.appendText("with class name: ")
            dataMatcher.describeTo(description)
        }

        public override fun matchesSafely(view: View) : Boolean {
            if (view !is AdapterView<*>) {
                return false
            }

            val adapter = view.adapter
            for (i in 0 until adapter.count) {
                if (dataMatcher.matches(adapter.getItem(i))) {
                    return true
                }
            }

            return false
        }
    }
}

Java

private static Matcher<View> withAdaptedData(final Matcher<Object> dataMatcher) {
    return new TypeSafeMatcher<View>() {

        @Override
        public void describeTo(Description description) {
            description.appendText("with class name: ");
            dataMatcher.describeTo(description);
        }

        @Override
        public boolean matchesSafely(View view) {
            if (!(view instanceof AdapterView)) {
                return false;
            }

            @SuppressWarnings("rawtypes")
            Adapter adapter = ((AdapterView) view).getAdapter();
            for (int i = 0; i < adapter.getCount(); i++) {
                if (dataMatcher.matches(adapter.getItem(i))) {
                    return true;
                }
            }

            return false;
        }
    };
}

次に必要なのは、onView() を使用して AdapterView を見つけることです。

Kotlin

fun testDataItemNotInAdapter() {
    onView(withId(R.id.list))
          .check(matches(not(withAdaptedData(withItemContent("item: 168")))))
    }
}

Java

@SuppressWarnings("unchecked")
public void testDataItemNotInAdapter() {
    onView(withId(R.id.list))
          .check(matches(not(withAdaptedData(withItemContent("item: 168")))));
    }
}

また、ID リストを持つアダプタビューに「item: 168」と等しいアイテムが存在する場合に失敗するアサーションも用意しています。

完全なサンプルについては、GitHub の AdapterViewTest.java クラスの testDataItemNotInAdapter() メソッドをご覧ください。

カスタム障害ハンドラの使用

Espresso のデフォルトの FailureHandler をカスタム対応に置き換えると、スクリーンショットの撮影や追加のデバッグ情報の受け渡しなど、追加または異なるエラー処理が可能になります。

CustomFailureHandlerTest の例は、カスタム障害ハンドラを実装する方法を示しています。

Kotlin

private class CustomFailureHandler(targetContext: Context) : FailureHandler {
    private val delegate: FailureHandler

    init {
        delegate = DefaultFailureHandler(targetContext)
    }

    override fun handle(error: Throwable, viewMatcher: Matcher<View>) {
        try {
            delegate.handle(error, viewMatcher)
        } catch (e: NoMatchingViewException) {
            throw MySpecialException(e)
        }

    }
}

Java

private static class CustomFailureHandler implements FailureHandler {
    private final FailureHandler delegate;

    public CustomFailureHandler(Context targetContext) {
        delegate = new DefaultFailureHandler(targetContext);
    }

    @Override
    public void handle(Throwable error, Matcher<View> viewMatcher) {
        try {
            delegate.handle(error, viewMatcher);
        } catch (NoMatchingViewException e) {
            throw new MySpecialException(e);
        }
    }
}

この障害ハンドラは、NoMatchingViewException ではなく MySpecialException をスローし、他のすべての障害は DefaultFailureHandler にデリゲートします。CustomFailureHandler は、テストの setUp() メソッドで Espresso に登録できます。

Kotlin

@Throws(Exception::class)
override fun setUp() {
    super.setUp()
    getActivity()
    setFailureHandler(CustomFailureHandler(
            ApplicationProvider.getApplicationContext<Context>()))
}

Java

@Override
public void setUp() throws Exception {
    super.setUp();
    getActivity();
    setFailureHandler(new CustomFailureHandler(
            ApplicationProvider.getApplicationContext()));
}

詳細については、FailureHandler インターフェースと Espresso.setFailureHandler() をご覧ください。

デフォルト以外のウィンドウを対象に指定する

Android では複数のウィンドウがサポートされます。通常、これはユーザーとアプリ デベロッパーにとって透過的ですが、オートコンプリート ウィンドウが検索ウィジェットのメイン アプリ ウィンドウの上に描画される場合など、複数のウィンドウが表示される場合もあります。Espresso では、デフォルトでヒューリスティックを使用して、操作する Window を推測します。ほとんどの場合、このヒューリスティックで十分ですが、まれに操作のターゲットにするウィンドウの指定が必要になることがあります。そのためには、独自のルート ウィンドウ マッチャーまたは Root マッチャーを用意します。

Kotlin

onView(withText("South China Sea"))
    .inRoot(withDecorView(not(`is`(getActivity().getWindow().getDecorView()))))
    .perform(click())

Java

onView(withText("South China Sea"))
    .inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView()))))
    .perform(click());

ViewMatchers の場合と同様に、RootMatchers のセットが用意されています。もちろん、独自の Matcher オブジェクトを実装することも可能です。

GitHub の MultipleWindowTest サンプルをご覧ください。

ヘッダーとフッターは、addHeaderView() メソッドと addFooterView() メソッドを使用して ListViews に追加されます。照合するデータ オブジェクトを Espresso.onData() が認識できるようにするには、プリセットのデータ オブジェクト値を 2 番目のパラメータとして addHeaderView()addFooterView() に渡します。次に例を示します。

Kotlin

const val FOOTER = "FOOTER"
...
val footerView = layoutInflater.inflate(R.layout.list_item, listView, false)
footerView.findViewById<TextView>(R.id.item_content).text = "count:"
footerView.findViewById<TextView>(R.id.item_size).text
        = data.size.toString
listView.addFooterView(footerView, FOOTER, true)

Java

public static final String FOOTER = "FOOTER";
...
View footerView = layoutInflater.inflate(R.layout.list_item, listView, false);
footerView.findViewById<TextView>(R.id.item_content).setText("count:");
footerView.findViewById<TextView>(R.id.item_size).setText(String.valueOf(data.size()));
listView.addFooterView(footerView, FOOTER, true);

その後、次のようにしてフッターのマッチャーを作成します。

Kotlin

import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.instanceOf
import org.hamcrest.Matchers.`is`

fun isFooter(): Matcher<Any> {
    return allOf(`is`(instanceOf(String::class.java)),
            `is`(LongListActivity.FOOTER))
}

Java

import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;

@SuppressWarnings("unchecked")
public static Matcher<Object> isFooter() {
    return allOf(is(instanceOf(String.class)), is(LongListActivity.FOOTER));
}

また、テストでのビューの読み込みは次のように簡単に行えます。

Kotlin

import androidx.test.espresso.Espresso.onData
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.sample.LongListMatchers.isFooter

fun testClickFooter() {
    onData(isFooter())
        .perform(click())

    // ...
}

Java

import static androidx.test.espresso.Espresso.onData;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.sample.LongListMatchers.isFooter;

public void testClickFooter() {
    onData(isFooter())
        .perform(click());

    // ...
}

コードサンプル全体を見る。GitHub の AdapterViewTest.javatestClickFooter() メソッドにあります。