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 に変換できることを覚えておいてください。

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

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 サンプルをご覧ください。

ListViews にヘッダーとフッターを追加するには、addHeaderView() メソッドと addFooterView() メソッドを使用します。マッチングするデータ オブジェクトを Espresso.onData() で指定するには、addHeaderView()addFooterView() の 2 番目のパラメータとして、事前設定済みのデータ オブジェクト値を渡してください。例:

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() メソッドをご覧ください。