Espressoを使ってAndroidのUIテストを行う方法を紹介します。
目次
Espressoとは
スマートフォンはタッチスクリーン上を指で操作するものが主流です。
画面上には操作性を損なわないように配置されたGUIパーツ(View)が並びます。アプリを開発すると、このGUIパーツを操作するUIテストが欠かせません。
EspressoはAndroidのUIをテストするために開発されたフレームワークです。Android公認のフレームワークになっています。
Espressoを使うことでUIの操作(クリックする、スワイプする、など)や、UIの状態チェック(ボタンが表示されている、文字が出力されている、など)を行うテストプログラムが記述できるようになります。
Espressoのさらなる情報はここを参照してください。
環境設定
Android Studioを使っているのであれば、プロジェクト(モジュール)を作成すると自動でEspresso環境を構築してくれます。なので、特に開発者側で行うことはありません。
環境が構築されるとライブラリの依存リストへ次の行が追加されます。
dependencies { .... androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' ... }
+--- androidx.test.espresso:espresso-core:3.2.0 +--- androidx.test:runner:1.2.0 | +--- androidx.annotation:annotation:1.0.0 -> 1.1.0 | +--- androidx.test:monitor:1.2.0 (*) | +--- junit:junit:4.12 (*) | \--- net.sf.kxml:kxml2:2.3.0 +--- androidx.test.espresso:espresso-idling-resource:3.2.0 +--- com.squareup:javawriter:2.1.1 +--- javax.inject:javax.inject:1 +--- org.hamcrest:hamcrest-library:1.3 | \--- org.hamcrest:hamcrest-core:1.3 +--- org.hamcrest:hamcrest-integration:1.3 | \--- org.hamcrest:hamcrest-library:1.3 (*) \--- com.google.code.findbugs:jsr305:2.0.1
EspressoはAnrdoidのUIテストを行うためのフレームワークです。Android上でテストを動かす必要がありますので、テストはインストゥルメントになります。
また、GUIパーツ(View)を表示するにはActivityを動かす必要があります。なのでActivityTestRuleの使用は必須です。
class SampleTest { @get:Rule var activityRule = ActivityTestRule(MainActivity::class.java, false) // ↑ GUIパーツ(View)を表示するためにActivityの起動は必須 @Test fun サンプル_test() { /* ここにEspressoのテストコードを記述 */ } }
public class SampleTest { @Rule public ActivityTestRule<MainActivity> mRule = new ActivityTestRule<>(MainActivity.class, false); // ↑ GUIパーツ(View)を表示するためにActivityの起動は必須 @Test public void サンプル_test(){ /* ここにEspressoのテストコードを記述 */ } }
テストを行う(onView編)
アプリの画面はツリー状にViewGroup(ConstraintLayout, LinearLayoutなど)やView(TextView,Bottonなど)が配置されています。
「GroupView内に子供のGroupViewやViewがあり、子供のGroupView内に孫のGroupViewやViewがあり、…続く」となっていて、その根元がDecorViewです。
このようなツリー構造からView(ViewGroupもViewのサブクラス)を検出して、テスト(操作と検証)を行うのがonView関数(メソッド)です。
onViewの構文は下記の通りです。
onView(ViewMatcher) // ツリーからViewを検出する .perform(ViewAction) // 検出されたViewを操作する .check(ViewAssertion) // 検出されたViewを検証する
@Test fun ツリー配置のViewをチェック_test() { onView(withId(R.id.button)).perform(click()) // ↑ ID:R.id.buttonを持つViewを検出し、クリックする onView(withId(R.id.editText)).perform(typeText("Hi!"), closeSoftKeyboard()) // ↑ ID:R.id.editTextを持つViewを検出し // テキストエリアに"Hi!"を入力、その後キーボードを閉じる onView(withId(R.id.textView)).check(matches(isDisplayed())) // ↑ ID:R.id.textViewを持つViewを検出し、表示されていることを確認する onView(withId(R.id.textView)).check(matches(withText("Hello World!"))) // ↑ ID:R.id.textViewを持つViewを検出し // テキストが”Hello World!"であることを確認する }
@Test public void ツリー配置のViewをチェック_test() { onView(withId(R.id.button)).perform(click()) // ↑ ID:R.id.buttonを持つViewを検出し、クリックする onView(withId(R.id.editText)).perform(typeText("Hi!"), closeSoftKeyboard()) // ↑ ID:R.id.editTextを持つViewを検出し // テキストエリアに"Hi!"を入力、その後キーボードを閉じる onView(withId(R.id.textView)).check(matches(isDisplayed())) // ↑ ID:R.id.textViewを持つViewを検出し、表示されていることを確認する onView(withId(R.id.textView)).check(matches(withText("Hello World!"))) // ↑ ID:R.id.textViewを持つViewを検出し // テキストが”Hello World!"であることを確認する }
ViewMatcherはViewを対象にしたHamcrestのMatcherで、条件にマッチしたViewが検出できます。
例えば、 withId(リソースID) は引数で指定したリソースIDを持つViewを検出します。
onViewで検出されたViewに対して、タッチスクリーン上を指で操作する事と同等な操作が行えます。
例えば、 click() は検出されたViewをクリックします。
onViewで検出されたViewにたいして、現在のViewの状態を検証します。
例えば、 maches(ViewMatcher) は「引数で指定した条件(ViewMatcher)に一致する状態であるか?」を調べます。
記述例で使ったViewMatcher/ViewAction/ViewAssertionは代表的なものです。その他、様々なものが使用できます。Espressoクイックリファレンスにまとめられているので参照してください。
テストを行う(onData編)
「アプリの画面はツリー状にViewが配置されている」とonView編で述べました。しかし、このツリーに含まれないものがあります。それは、AdapterView(Spinner,ListViewなど)が管理し、リスト状に配置されるアイテムのViewです。
このようなリスト構造からViewを検出してテスト(操作と検証)を行うのがonData関数(メソッド)です。
onDataの構文は下記の通りです。
onData(ObjectMatcher) // リストからアイテムのViewを検出する .DataOptions // onDataの補助条件(必要な場合のみ) .perform(ViewAction) // 検出されたViewを操作する .check(ViewAssertion) // 検出されたViewを検証する
@Test fun リスト配置のViewをチェック_test() { onData(`is`("Tue.")).perform(click()) // ↑ 要素が"Tue."であるアイテムのViewを検出し、クリックする onData(hasEntry("Name", "Wed.")).perform(click()) // ↑ キー"Name"で値"Wed."のエントリーを持つアイテムのViewを検出し、クリックする onData(anything()).atPosition(2) .check(matches(hasDescendant(withText("Wed.")))) // ↑ データのインデックスが2であるアイテムのViewを検出し // 要素が"Wed."であることを確認する // ※hasDescendant(ViewMatcher)は引数にマッチする子Viewの有無を調べる }
@Test public void リスト配置のViewをチェック_test() { onData(is("Tue.")).perform(click()); // ↑ 要素が"Tue."であるアイテムのViewを検出し、クリックする onData(hasEntry("Name", "Wed.")).perform(click()); // ↑ キー"Name"で値"Wed."のエントリーを持つアイテムのViewを検出し、クリックする onData(anything()).atPosition(2) .check(matches(hasDescendant(withText("Wed.")))); // ↑ データのインデックスが2であるアイテムのViewを検出し // 要素が"Wed."を持つことを確認する // ※hasDescendant(ViewMatcher)は引数にマッチする子Viewの有無を調べる }
※ inAdapterViewはDataOptionsです。
ObjectMatcherはHamcrestのMatcherで、条件にマッチした要素を持つアイテムのViewが検出できます。
[ ArrayAdapterの例 ]
例えば、下記に示すようなArrayAdapterが設定されたAdapterViewだったとします。
val arrayData = arrayOf( // データはStringの配列 "Mon.", "Tue.", "Wed.", "Thur.", // アイテム "Fri.", "Sat.", "Sun" // アイテム ) val arrayAdapter = ArrayAdapter( this, android.R.layout.simple_expandable_list_item_1, arrayData )
onData(`is`("Tue."))
String[] _ArrayData = new String[]{ "Mon.", "Tue.", "Wed.", "Thur.", "Fri.", "Sat.", "Sun" }; ArrayAdapter _ArrayAdapter = new ArrayAdapter( this, android.R.layout.simple_expandable_list_item_1, _ArrayData );
onData(is("Tue."))
このonDataは要素が「Tue.」であるアイテムのViewを検出します。
onDataが検出するのは android.R.layout.simple_expandable_list_item_1 のView(中はTextView)です。
[ SimpleAdapterの例 ]
例えば、下記に示すようなSimpleAdapterが設定されたAdapterViewだったとします。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/list_item" android:orientation="horizontal"> <TextView android:id="@+id/textNum" android:text="No." /> <TextView android:id="@+id/textWeek" android:text="WeekName" /> </LinearLayout>
val listData = listOf( // データはMapのリスト mapOf("Num" to 0, "Name" to "Mon."), // アイテム mapOf("Num" to 1, "Name" to "Tue."), // アイテム mapOf("Num" to 2, "Name" to "Wed."), // アイテム mapOf("Num" to 3, "Name" to "Thur."), // アイテム mapOf("Num" to 4, "Name" to "Fri."), // アイテム mapOf("Num" to 5, "Name" to "Sat."), // アイテム mapOf("Num" to 6, "Name" to "Sun.") // アイテム ) val simpleAdapter = SimpleAdapter( this, listData, R.layout.list_item, arrayOf("Num", "Name"), intArrayOf(R.id.textNum, R.id.textWeek) )
onData(hasEntry("Name", "Wed."))
List<Map<String, Object>> _ListData = new ArrayList<>(); // データはMapのリスト Map<String, Object> _Map0 = new HashMap<>(); // アイテム _Map0.put("Num", 0);_Map0.put("Name", "Mon."); _ListData.add(_Map0); Map<String, Object> _Map1 = new HashMap<>(); // アイテム _Map1.put("Num", 1);_Map1.put("Name", "Tue."); _ListData.add(_Map1); Map<String, Object> _Map2 = new HashMap<>(); // アイテム _Map2.put("Num", 2);_Map2.put("Name", "Wed."); _ListData.add(_Map2); Map<String, Object> _Map3 = new HashMap<>(); // アイテム _Map3.put("Num", 3);_Map3.put("Name", "Thur."); _ListData.add(_Map3); Map<String, Object> _Map4 = new HashMap<>(); // アイテム _Map4.put("Num", 4);_Map4.put("Name", "Fri."); _ListData.add(_Map4); Map<String, Object> _Map5 = new HashMap<>(); // アイテム _Map5.put("Num", 5);_Map5.put("Name", "Sat."); _ListData.add(_Map5); Map<String, Object> _Map6 = new HashMap<>(); // アイテム _Map6.put("Num", 6);_Map6.put("Name", "Sun."); _ListData.add(_Map6); SimpleAdapter _SimpleAdapter = new SimpleAdapter( this, _ListData, R.layout.list_item, new String[]{"Num", "Name"}, new int[]{R.id.textNum, R.id.textWeek} );
onData(hasEntry("Name", "Wed."))
このonDataはキーが「Name」で値が「Wed.」のエントリーを持つアイテムのViewを検出します。
onDataが検出するのは R.layout.list_item のView(中はLinearLayout)です。
例えば、 atPosition(Integer) はアイテムがインデックス位置のデータであることを指定します。引数はインデックス番号です。
例えば、 inAdapterView(ViewMatcher) はonDataが対象にするAdapterViewを指定します。引数はAdapterViewの条件を示すViewMatcherです。
onDataで検出されたViewに対して、タッチスクリーン上を指で操作する事と同等な操作が行えます。
例えば、 click() は検出されたViewをクリックします。
onDataで検出されたViewにたいして、現在のViewの状態を検証します。
例えば、 maches(ViewMatcher) は「引数で指定した条件(ViewMatcher)に一致する状態であるか?」を調べます。
記述例で使ったViewMatcher/ViewAction/ViewAssertionは代表的なものです。その他、様々なものが使用できます。Espressoクイックリファレンスにまとめられているので参照してください。
Espressoについて追記
Kotlinはisが予約語
Espressoは引数にHamcrestのMatcherを用いています。Kotlinは「is」が予約語なのでMatcherの「is」が直に使えません。よって「`is`」と表現します。
美しくない表現ですが妥協せざる負えません。悲しい!
ListView, GridViewはLegacy扱い
ListViewやGridViewはLegacy扱いになりました。代わりにRecyclerViewの使用が推奨されています。
なので、今後開発されるアプリのテストでonDataの使う場面はSpinnerくらいになってしまうでしょう。
RecyclerViewは別のテスト方法が用意されています。
ドキュメントはAdapterViewが一つの場合
Googleが提供しているEspressoのドキュメントに次のような記述が見られます。
onData(allOf(`is`(instanceOf(Map.class)), hasEntry(equalTo("STR"), `is`("item: 50")))).perform(click())
これは、画面にAdapterViewが一つの時の記述だと思います。
複数ある時は、対象にするAdapterViewを選定するためにinAdapterViewが必要になります。
また、AdapterViewが一つであるならば、instansOfでMapであることを条件にする必要はあるのでしょうか?!
管理されるアイテムは全て同じ構成の要素を持つはずです。同じ構成ならば全てのアイテムでinstanceOfはマッチします。全てマッチするならば条件は無駄です。
「昔はこのように書いていた。今は仕様が変更になった。」というだけかも知れません。