このドキュメントでは、Espresso API を使用して一般的な自動テストタスクを実施する方法について説明します。
Espresso API を使用すると、ユーザーがアプリを操作する中で行うこと(UI 要素を見つけて操作する)を考えながらテストを作成できます。また、Espresso フレームワークでは、アプリのアクティビティとビューに直接アクセスできないようになっています。このようなオブジェクトを UI スレッド外で保持して操作すると、テストの信頼性が失われるおそれがあるためです。したがって、Espresso API には getView()
や getCurrentActivity()
のようなメソッドはありません。
そうしたメソッドがなくても、ViewAction
と ViewAssertion
の独自のサブクラスを実装することにより、ビューを安全に操作できます。
API コンポーネント
Espresso の主なコンポーネントは次のとおりです。
- Espresso –
onView()
とonData()
を介してビューとやり取りするためのエントリ ポイント。必ずしもビューに結び付けられていない API も公開します(pressBack()
など)。 - ViewMatchers -
Matcher<? super View>
インターフェースを実装するオブジェクトのコレクション。この中の 1 つ以上のオブジェクトをonView()
メソッドに渡して、現在のビュー階層内のビューを特定します。 - ViewAction -
click()
など、ViewInteraction.perform()
メソッドに渡すことができるViewAction
オブジェクトのコレクション。 - ViewAssertions -
ViewInteraction.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 マッチャーが使用されます。このマッチャーにより、現在のビュー階層内でビューが 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
を指定すれば十分です。 - 対象のビューが
ListView
、GridView
、Spinner
などのように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
に Button
と TextView
が含まれています。ボタンがクリックされると、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
のカスタム実装では、継承コントラクトに違反すると、onData()
メソッドで問題が発生する可能性があります(特に getItem()
API の場合)。このケースでは、アプリコードのリファクタリングが最善の措置です。それができない場合は、カスタムのマッチング 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 のテストに関するその他の情報については、次のリソースをご覧ください。
サンプル
- CustomMatcherSample: Espresso の機能を拡張して、
EditText
オブジェクトの hint プロパティのマッチングを行う方法を示すサンプル。 - RecyclerViewSample: Espresso 用の
RecyclerView
アクション。 - その他のサンプル