Espresso の基本

このドキュメントでは、Espresso API を使用して一般的な自動テストタスクを実施する方法について説明します。

Espresso API を使用すると、ユーザーがアプリとやり取りする中で行うこと(UI 要素を見つけて操作する)を考えながらテストを作成できます。また、Espresso フレームワークでは、アプリのアクティビティとビューに直接アクセスできないようになっています。これは、このようなオブジェクトを UI スレッド以外から直接操作すると、テストが不安定になるおそれがあるためです。したがって、Espresso API には getView()getCurrentActivity() などのメソッドはありません。それでも、ViewActionViewAssertion で独自のサブクラスを実装することにより、ビューを安全に操作できます。

API コンポーネント

Espresso の主なコンポーネントは次のとおりです。

  • Espresso – ビューとやり取り(onView()onData() を介して)するためのエントリ ポイント。表示される API は、必ずしもビューに関連付くものとは限りません(pressBack() など)。
  • ViewMatchersMatcher<? super View> インターフェースを実装するオブジェクトのコレクション。この中の 1 つ以上を onView() メソッドに渡して、現在のビュー階層の中でのビューの場所を特定できます。
  • ViewActionsViewInteraction.perform() メソッドに渡すことのできる ViewAction オブジェクトのコレクション(click() など)。
  • ViewAssertionsViewInteraction.check() メソッドに渡すことのできる ViewAssertion オブジェクトのコレクション。多くの場合、使用するのは matches アサーションで、ビュー マッチャーを使って選択中のビューの状態に対してアサーションを行います。

例:

Kotlin

    // withId(R.id.my_view) is a ViewMatcher
    // click() is a ViewAction
    // matches(isDisplayed()) is a ViewAssertion
    onView(withId(R.id.my_view))
        .perform(click())
        .check(matches(isDisplayed()))
    

Java

    // withId(R.id.my_view) is a ViewMatcher
    // click() is a ViewAction
    // matches(isDisplayed()) is a ViewAssertion
    onView(withId(R.id.my_view))
        .perform(click())
        .check(matches(isDisplayed()));
    

ビューの特定

ほとんどの場合、onView() メソッドでは Hamcrest マッチャーが使用されます。通常は、Hamcrest マッチャーによりビュー階層内でビューが 1 つに絞り込まれます。マッチャーは非常に有効で、Mockito や JUnit の使用経験があればすぐに使いこなせます。Hamcrest マッチャーになじみがない場合は、このプレゼンテーションをまずご覧ください。

たいていの場合、目的のビューには一意の R.id があり、単純な withId マッチャーでビューを絞り込めます。しかし、テスト開発時には、当然のこととして R.id を特定できないケースが多くあります。たとえば、あるビューには R.id がなかったり、あっても一意の R.id でなかったりします。このような場合は、ビューにアクセスするために通常使用する findViewById() が機能しないため、標準的なインストゥルメンテーション テストの作成が複雑で困難になります。場合によっては、ビューを保持するアクティビティやフラグメントのプライベート メンバーにアクセスしたり、既知の R.id を持つコンテナを見つけて、そこから特定のビューのコンテンツに移動したりする必要があります。

Espresso では、ビューの絞り込みに、既存の ViewMatcher オブジェクトや独自のカスタム オブジェクトのいずれかを使用できるようにすることで、この問題を解決しています。

R.id によるビューの特定は、onView() を呼び出すことで簡単に行えます。

Kotlin

    onView(withId(R.id.my_view))
    

Java

    onView(withId(R.id.my_view));
    

複数のビューが同じ R.id 値を持つ場合があります。このような状況で、その R.id を使用すると、AmbiguousViewMatcherException などの例外が発生します。例外メッセージには、現在のビュー階層がテキストで表示されており、一意でない R.id と一致する複数のビューを検索により探し出せます。

    java.lang.RuntimeException:
    androidx.test.espresso.AmbiguousViewMatcherException
    This matcher matches multiple views in the hierarchy: (withId: is <123456789>)

    ...

    +----->SomeView{id=123456789, res-name=plus_one_standard_ann_button,
    visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
    window-focus=true, is-focused=false, is-focusable=false, enabled=true,
    selected=false, is-layout-requested=false, text=,
    root-is-layout-requested=false, x=0.0, y=625.0, child-count=1}
    ****MATCHES****
    |
    +------>OtherView{id=123456789, res-name=plus_one_standard_ann_button,
    visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
    window-focus=true, is-focused=false, is-focusable=true, enabled=true,
    selected=false, is-layout-requested=false, text=Hello!,
    root-is-layout-requested=false, x=0.0, y=0.0, child-count=1}
    ****MATCHES****
    

表示されたビューのさまざまな属性を調べると、一意に特定可能なプロパティが見つかる場合があります。上記の例では、一方のビューに "Hello!" というテキストが入っています。これを利用すれば、次のようにしてマッチャーの組み合わせによる絞り込み検索が可能です。

Kotlin

    onView(allOf(withId(R.id.my_view), withText("Hello!")))
    

Java

    onView(allOf(withId(R.id.my_view), withText("Hello!")));
    

どのマッチャーも、not 関数を使用することで、値を反転させることができます。

Kotlin

    onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))))
    

Java

    onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))));
    

Espresso で利用できるビュー マッチャーについては、ViewMatchers をご覧ください。

留意事項

  • アプリを正しく動作させるためには、ユーザーが操作できるすべてのビューに、説明となるテキストまたはコンテンツの説明文を含める必要があります。詳細については、アプリをより使いやすくする方法をご覧ください。withText()withContentDescription() を使用してビューを絞り込めない場合は、アプリの使いやすさに問題があると考えてください。
  • 必要なビューを検索できるようにするためにマッチャーに説明を付ける際は、必要最低限のものにしてください。Espresso フレームワークに不要な負荷がかかるため、過度な指定を避けます。たとえば、ビューがそのテキストで一意に特定できるのであれば、TextView から割り当て可能であることまで指定する必要はありません。多くのビューでは、R.id を指定すれば十分です。
  • 対象のビューが ListViewGridViewSpinner など AdapterView の内部にあると、onView() メソッドが機能しないことがあります。このような場合は、代わりに onData() を使用する必要があります。

ビューでのアクションの実施

対象のビューに適合するマッチャーが見つかったら、perform メソッドを使用して、そのビューで ViewAction インスタンスを実施できます。

たとえば、次のようにしてそのビューをクリックします。

Kotlin

    onView(...).perform(click())
    

Java

    onView(...).perform(click());
    

次のように、1 回の perform 呼び出しで複数のアクションを実施できます。

Kotlin

    onView(...).perform(typeText("Hello"), click())
    

Java

    onView(...).perform(typeText("Hello"), click());
    

作業対象のビューが ScrollView(垂直または水平)内にある場合は、click()typeText() など、ビューを表示しておく必要があるアクションの前に、scrollTo() を使用するようにしてください。次のようにして、ビューが確実に表示されてからその他のアクションに進みます。

Kotlin

    onView(...).perform(scrollTo(), click())
    

Java

    onView(...).perform(scrollTo(), click());
    

Espresso で利用できるビュー アクションについては、ViewActions をご覧ください。

ビュー アサーションの確認

check() メソッドを使用すると、選択中のビューにアサーションを適用できます。よく使用されるのは matches() アサーションです。これは、ViewMatcher オブジェクトを使用して、選択中のビューの状態に対してアサーションを行うものです。

たとえば、ビューにテキスト "Hello!" が含まれることを確認するには次のようにします。

Kotlin

    onView(...).check(matches(withText("Hello!")))
    

Java

    onView(...).check(matches(withText("Hello!")));
    

ビューのコンテンツに "Hello!" が含まれているかを、アサーションで確認する場合の不適切な例を次に示します。

Kotlin

    // Don't use assertions like withText inside onView.
    onView(allOf(withId(...), withText("Hello!"))).check(matches(isDisplayed()))
    

Java

    // Don't use assertions like withText inside onView.
    onView(allOf(withId(...), withText("Hello!"))).check(matches(isDisplayed()));
    

ただし、テキスト "Hello!" を含むビューが表示されていることをアサーションで確認する場合(たとえば、ビューの表示フラグを変更した後など)であれば、このコードで問題ありません。

ビュー アサーションの簡単なテスト

この例では、SimpleActivity には ButtonTextView が含まれています。ボタンがクリックされると、TextView のコンテンツが "Hello Espresso!" に変わります。

Espresso では、これを次のようにテストします。

ボタンのクリック

最初のステップとして、ボタンの特定に役立つプロパティを探します。SimpleActivity 内のボタンに一意の R.id がある場合は、次のようにします。

Kotlin

    onView(withId(R.id.button_simple))
    

Java

    onView(withId(R.id.button_simple));
    

次のようにすると、クリックが行えます。

Kotlin

    onView(withId(R.id.button_simple)).perform(click())
    

Java

    onView(withId(R.id.button_simple)).perform(click());
    

TextView のテキストの確認

確認対象のテキストを含む TextView にも一意の R.id がある場合は、次のようにします。

Kotlin

    onView(withId(R.id.text_simple))
    

Java

    onView(withId(R.id.text_simple));
    

次のようにすると、コンテンツのテキストが確認できます。

Kotlin

    onView(withId(R.id.text_simple)).check(matches(withText("Hello Espresso!")))
    

Java

    onView(withId(R.id.text_simple)).check(matches(withText("Hello Espresso!")));
    

アダプター ビューでのデータ読み込みの確認

AdapterView は、データをアダプターから動的に読み込む特別なタイプのウィジェットです。AdapterView の最も一般的な例は ListView です。LinearLayout のような静的なウィジェットとは異なり、AdapterView では、タイミングによってビュー階層に読み込まれているのが子要素のサブセットだけである場合があります。通常の onView() 検索では、検索の時点で読み込まれていないビューは見つけられません。

Espresso では、別途 onData() エントリ ポイントを用意することでこの問題を解決しています。該当するアダプター項目を先に読み込んでおき、アダプターやその子要素に対する操作に先だって、対象に含めておくことができます。

警告: AdapterView のカスタム実装では、継承コントラクト(特に getItem() API)を破ると、onData() メソッドで問題が発生する可能性があります。この場合の処置としては、アプリのコードをリファクタリングするのが最適です。それができない場合は、マッチング カスタム AdapterViewProtocol を実装します。詳細については、Espresso に用意されているデフォルトの AdapterViewProtocols クラスをご覧ください。

アダプター ビューの簡単なテスト

ここでは、簡単なテストにより onData() の使用方法を説明します。SimpleActivity には、コーヒー飲料の種類を表す項目をいくつか含んだ Spinner があります。ある項目を選択すると、そこにある TextView の内容が "One %s a day!" に変わります(ここで、%s は選択した項目を表します)。

このテストでは、Spinner を開いて特定の項目を選択した後、TextView にその項目が含まれていることを確認します。Spinner クラスは AdapterView がベースのため、項目のマッチングには onView() ではなく onData() を使用することが推奨されます。

項目の選択肢の表示

Kotlin

    onView(withId(R.id.spinner_simple)).perform(click())
    

Java

    onView(withId(R.id.spinner_simple)).perform(click());
    

項目の選択

項目を選択できるよう、Spinner でコンテンツから ListView が作成されます。このビューは非常に長くなる可能性があり、要素がビュー階層に含まれないことも考えられます。そのため、onData() を使用して目的の要素を強制的にビュー階層に含めます。Spinner 内の項目は文字列のため、次のようにして文字列 "Americano" と等しい項目にマッチさせます。

Kotlin

    onData(allOf(`is`(instanceOf(String::class.java)),
            `is`("Americano"))).perform(click())
    

Java

    onData(allOf(is(instanceOf(String.class)), is("Americano"))).perform(click());
    

テキストが正しいことの確認

Kotlin

    onView(withId(R.id.spinnertext_simple))
        .check(matches(withText(containsString("Americano"))))
    

Java

    onView(withId(R.id.spinnertext_simple))
        .check(matches(withText(containsString("Americano"))));
    

デバッグ

テストが失敗した場合、Espresso からデバッグに役立つ情報を入手できます。

ログ

Espresso では、すべてのビュー アクションが logcat に記録されます。例:

    ViewInteraction: Performing 'single click' action on view with text: Espresso
    

ビュー階層

Espresso では、onView() が失敗した場合、例外メッセージの中にビュー階層が示されます。

  • onView() で目的のビューが見つからなかった場合は、NoMatchingViewException がスローされます。例外メッセージ中のビュー階層を調べることにより、マッチャーに一致するビューが確認できなかった理由を分析できます。
  • onView() で、マッチャーに一致するビューが複数見つかった場合は、AmbiguousViewMatcherException がスローされます。ビュー階層とともに、一致したすべてのビューが MATCHES ラベル付きで示されます。
    java.lang.RuntimeException:
    androidx.test.espresso.AmbiguousViewMatcherException
    This matcher matches multiple views in the hierarchy: (withId: is <123456789>)

    ...

    +----->SomeView{id=123456789, res-name=plus_one_standard_ann_button,
    visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
    window-focus=true, is-focused=false, is-focusable=false, enabled=true,
    selected=false, is-layout-requested=false, text=,
    root-is-layout-requested=false, x=0.0, y=625.0, child-count=1}
    ****MATCHES****
    |
    +------>OtherView{id=123456789, res-name=plus_one_standard_ann_button,
    visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
    window-focus=true, is-focused=false, is-focusable=true, enabled=true,
    selected=false, is-layout-requested=false, text=Hello!,
    root-is-layout-requested=false, x=0.0, y=0.0, child-count=1}
    ****MATCHES****
    

複雑なビュー階層やウィジェットの想定外の動作に対処する場合、Android Studio の Hierarchy Viewer を使用すると有用な情報が得られます。

アダプター ビューの警告

Espresso では、ユーザーに対して AdapterView ウィジェットの存在を示す警告が示されます。onView() 操作の結果、NoMatchingViewException がスローされ、ビュー階層に AdapterView ウィジェットが存在する場合は、onData() を使用することで通常は解決できます。例外メッセージには、アダプター ビューのリストを示す警告が含まれています。この情報を使用して onData() を呼び出し、目的のビューを読み込みます。

参考情報

Espresso を使用した Android のテストに関するその他の情報については、次のリソースをご覧ください。

サンプル