tl;dr
Espresso recipes読もう。
単純な場合
例えばSpinnerの選択肢の「hoge」をクリックしたい場合、セオリー通りに書くなら次のような感じです。
onData(allOf(is(instanceOf(String.class)), is("hoge"))).perform(click());
AmbiguousViewMatcherExceptionとの戦い
普通はこれで全く問題ないのですが、どうしても同じ画面内に「hoge」を表示している個所が複数あってAmbiguousViewMatcherExceptionになってしまう場合があって頭を抱えました。
Matcher<String>とMatcher<View>を同居させることはできないので、「あるViewの子View」みたいな条件を増やすことはできません。
Spinnerをクリックした時のポップアップのView Herarchyを調べてみると次のような感じです。
View Hierarchy
+>PopupDecorView
|
+->PopupBackgroundView
|
+-->DropDownListView
|
+--->AppCompatCheckedTextView
|
+--->AppCompatCheckedTextView
|
+--->AppCompatCheckedTextView
|
以下略
ずらっと並んだAppCompatCheckedTextViewの中に目的の選択肢がありますので、以下のようなコードで叩くようにしました。
onView(allOf(withText("hoge"), withParent(withClassName(endsWith("ListView"))), withClassName(endsWith("TextView")))).perform(click());
すると、最初の方の選択肢だった場合は問題なくクリックできますが、選択肢がとても多いSpinnerだった場合は一定のインデックス(画面に完全に表示しきれていないもの)以後は以下のようなエラーになってしまいました。
Error performing 'single click' on view '(with text: is "hoge" and has parent matching: with class name: a string ending with "ListView" and with class name: a string ending with "TextView")'.
ViewActions.click()のドキュメントには「must be displayed on screen」とありますので、表示できていないViewで実行できないのは当然です。ここで単純にscrollTo()すればいいんじゃね?とやると「NoMatchingViewException: No views in hierarchy found matching」になります。PopupだからActivityのView Hierarchyにないからダメということでしょうか。
最早この時点で「画面に表示しきれないくらいのたくさんの選択肢をSpinnerに表示するんじゃない」と言われている気がします。とはいえ目の前に生きたプロダクトがありそいつに対してテストをしなくてはなりません。こういう時は困ったときのEspresso recipesです。Using inRoot to target non-default windowsというぴったりのものがありました。これを使って冒頭に書いたコードを直します。
onData(allOf(is(instanceOf(String.class)), is("hoge")))
.inRoot(withDecorView(not(is(activityTestRule.getActivity().getWindow().getDecorView()))))
.perform(click());
今度は問題なくすべてのテストが完遂しました。Spinnerに全く同じ選択肢が複数あることはあり得ないはずなのでこれでよさそうです。そもそもNoMatchingViewExceptionを吐いたときに「you may need to use Espresso.onData to load it from one of the following AdapterViews:android.widget.ListPopupWindow$DropDownListView{...」みたいな警告が出るのでonViewを使うのは筋悪でしょう。
回り道をしましたが、最初からレシピを読んでおけばよかっただけの話でした。