EspressoのonDataはAdapterView(ListViewやSpinnerなど)が管理するアイテムのViewを検出します。
画面に複数のAdapterViewがある場合に、onDataでテストを行う方法を紹介します。単数の場合と違い、対象にするAdapterViewを指定する必要が出てきます。
複数のAdapterViewがある場合
画面に複数のAdapterView(ListViewやSpinnerなど)が存在するアプリがあるとします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | < androidx.constraintlayout.widget.ConstraintLayout android:layout_width = "match_parent" android:layout_height = "match_parent" tools:context = ".SpiLisActivity" > < LinearLayout android:id = "@+id/layGroup" android:layout_width = "280dp" android:orientation = "horizontal" > < ListView android:id = "@+id/lisSimple" android:layout_height = "wrap_content" /> < Spinner android:id = "@+id/spiArray" android:layout_height = "wrap_content" /> </ LinearLayout > </ androidx.constraintlayout.widget.ConstraintLayout > |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | val spiAdapter = ArrayAdapter( this , android.R.layout.simple_expandable_list_item_1, arrayOf( "Hop" , "Step" , "Jump" ) ) val _SpiArray: Spinner = findViewById(R.id.spiArray) _SpiArray.adapter = spiAdapter val lisAdapter = ArrayAdapter( this , android.R.layout.simple_expandable_list_item_1, arrayOf( "one" , "two" , "three" ) ) val _LisSimple: ListView = findViewById(R.id.lisSimple) _LisSimple.adapter = lisAdapter |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | ArrayAdapter _SpiAdapter = new ArrayAdapter( this , android.R.layout.simple_expandable_list_item_1, new String[]{ "Hop" , "Step" , "Jump" } ); Spinner _SpiArray = findViewById(R.id.spiArray); _SpiArray.setAdapter(_SpiAdapter); ArrayAdapter _LisAdapter = new ArrayAdapter( this , android.R.layout.simple_expandable_list_item_1, new String[]{ "one" , "two" , "three" } ); ListView _LisSimple = findViewById(R.id.lisSimple); _LisSimple.setAdapter(_LisAdapter); |
このアプリのAdapterViewをUIテストするために、次のようにonDataを使うとテストが失敗します。
1 2 3 4 | @Test fun multi_test() { onData(`is`( "two" )).perform(click()) } |
1 2 3 4 | @Test public void multi_test() { onData(is( "two" )).perform(click()); } |
原因はonDataが対象のAdapterViewを判別できないためです。
テストのログに、その理由が出力されます。着目点は”****MATCHES****”の付いているViewです。ListViewとSpinnerに付いています。つまり、ListViewとSpinnerのどちらのAdapterViewを対象にするか、判別できないことを告げいるのです。
androidx.test.espresso.AmbiguousViewMatcherException: 'is assignable from class: class android.widget.AdapterView' matches multiple views in the hierarchy. Problem views are marked with '****MATCHES****' below. View Hierarchy: +>DecorView{id=-1, v=VISIBLE, child=2} +->LinearLayout{id=-1, v=VISIBLE, child=2} +-->ViewStub{id=16909288, res=action_mode_bar_stub, v=GONE, } +-->FrameLayout{id=-1, v=VISIBLE, child=1} +--->ActionBarOverlayLayout{id=2131165262, res=decor_content_parent, v=VISIBLE, child=2} +---->ContentFrameLayout{id=16908290, res=content, v=VISIBLE, child=1} +----->ConstraintLayout{id=-1, v=VISIBLE, child=1} +------>LinearLayout{id=2131165286, res=layGroup, v=VISIBLE, child=2} +------->ListView{id=2131165291, res=lisSimple, v=VISIBLE, child=3} ****MATCHES**** +-------->AppCompatTextView{id=16908308, res=text1, v=VISIBLE, text=one} +-------->AppCompatTextView{id=16908308, res=text1, v=VISIBLE, text=two} +-------->AppCompatTextView{id=16908308, res=text1, v=VISIBLE, text=three} +------->AppCompatSpinner{id=2131165336, res=spiArray, v=VISIBLE, child=1} ****MATCHES**** +-------->AppCompatTextView{id=16908308, res=text1, v=VISIBLE, text=Hop} +---->ActionBarContainer{id=2131165225, res=action_bar_container, v=VISIBLE, child=2} +----->Toolbar{id=2131165223, res=action_bar, v=VISIBLE, child=2} +------>AppCompatTextView{id=-1, v=VISIBLE, text=Espresso} +------>ActionMenuView{id=-1, v=VISIBLE, child=0} +----->ActionBarContextView{id=2131165231, res=action_context_bar, v=GONE, child=0} +->View{id=16908335, res=statusBarBackground, v=VISIBLE, }
※上記のログは実際の出力と異なり、一部が省略されています。
これを解決するために、DataOptionsのinAdapterView(ViewMatcher)で、対象のAdapterViewを指定します。
1 2 3 4 5 6 | @Test fun multi_test() { onData(`is`( "two" )) .inAdapterView(withId(R.id.lisSimple)) .perform(click()) } |
1 2 3 4 5 6 | @Test public void multi_test() { onData(is( "two" )) .inAdapterView(withId(R.id.lisSimple)) .perform(click()); } |
inAdapterView()の引数はViewMatcherで条件を指定する形になっています。もっとも有効な条件はwithId(リソースID)でしょう。
これで、onDataによるアイテムのViewの検出が可能になります。
Spinnerは特殊(モーダル)
複数のAdapterViewがある場合でもSpinnerは特殊です。
SpinnerはDataOptionsのinAdapterView(ViewMatcher)が必要ありません。次のようにonDataを使うとテストは成功します。※アプリは前章と同じ
1 2 3 4 5 | @Test fun spinner_test() { onView(withId(R.id.spiArray)).perform(click()) // Spinnerを開く onData(`is`( "Jump" )).perform(click()) } |
1 2 3 4 5 | @Test public void spinner_test() { onView(withId(R.id.spiArray)).perform(click()); // Spinnerを開く onData(is( "Jump" )).perform(click()); } |
SpinnerをUIテストするためには、onDataの前に閉じた状態のSpinnerを開かなければなりません。
Spinnerを開くとViewのツリー構造が変わります。モーダルウィンドウ的な処理が期待されているためであると思われます。
【Spinnerが閉じた状態のツリー構造】
View Hierarchy:
+>DecorView{id=-1, v=VISIBLE, child=2}
+->LinearLayout{id=-1, v=VISIBLE, child=2}
+-->ViewStub{id=16909288, res=action_mode_bar_stub, v=GONE, }
+-->FrameLayout{id=-1, v=VISIBLE, child=1}
+--->ActionBarOverlayLayout{id=2131165262, res=decor_content_parent, v=VISIBLE, child=2}
+---->ContentFrameLayout{id=16908290, res=content, v=VISIBLE, child=1}
+----->ConstraintLayout{id=-1, v=VISIBLE, child=1}
+------>LinearLayout{id=2131165286, res=layGroup, v=VISIBLE, child=2}
+------->ListView{id=2131165291, res=lisSimple, v=VISIBLE, child=3}
+-------->AppCompatTextView{id=16908308, res=text1, v=VISIBLE, text=one}
+-------->AppCompatTextView{id=16908308, res=text1, v=VISIBLE, text=two}
+-------->AppCompatTextView{id=16908308, res=text1, v=VISIBLE, text=three}
+------->AppCompatSpinner{id=2131165336, res=spiArray, v=VISIBLE, child=1}
+-------->AppCompatTextView{id=16908308, res=text1, v=VISIBLE, text=Hop}
+---->ActionBarContainer{id=2131165225, res=action_bar_container, v=VISIBLE, child=2}
+----->Toolbar{id=2131165223, res=action_bar, v=VISIBLE, child=2}
+------>AppCompatTextView{id=-1, v=VISIBLE, text=Espresso}
+------>ActionMenuView{id=-1, v=VISIBLE, child=0}
+----->ActionBarContextView{id=2131165231, res=action_context_bar, v=GONE, child=0}
+->View{id=16908335, res=statusBarBackground, v=VISIBLE, }
【Spinnerが開いた状態のツリー構造】
View Hierarchy:
+>PopupDecorView{id=-1, v=VISIBLE, child=1}
+->PopupBackgroundView{id=-1, v=VISIBLE, child=1}
+-->DropDownListView{id=-1, v=VISIBLE, child=3}
+--->AppCompatTextView{id=16908308, res=text1, v=VISIBLE, text=Hop}
+--->AppCompatTextView{id=16908308, res=text1, v=VISIBLE, text=Step}
+--->AppCompatTextView{id=16908308, res=text1, v=VISIBLE, text=Jump}
開いたウィンドウを適切に操作するか、キャンセルするか、どちらかの操作を行わない限り、制御をもとのプロセスに戻さないものをいいます。
Spinnerが開かれると、そこに存在するAdapterViewはSpinner(DropDownListView)のみになるので、inAdapterViewは必要なくなるのです。