EspressoでAndroidのUIテストを行う

投稿日:  更新日:

Espressoを使ってAndroidのUIテストを行う方法を紹介します。

スポンサーリンク

Espressoとは

スマートフォンはタッチスクリーン上を指で操作するものが主流です。

画面上には操作性を損なわないように配置されたGUIパーツ(View)が並びます。アプリを開発すると、このGUIパーツを操作するUIテストが欠かせません。

Espressoマスコット
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上でテストを動かす必要がありますので、テストはインストゥルメントになります。

Espressoのコードを記述する場所

また、GUIパーツ(View)を表示するにはActivityを動かす必要があります。なのでActivityTestRuleの使用は必須です。

KotlinJava
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です。

onViewはツリー構造からViewを検出します

このようなツリー構造からView(ViewGroupもViewのサブクラス)を検出して、テスト(操作と検証)を行うのがonView関数(メソッド)です。

onViewの構文は下記の通りです。

onView(ViewMatcher)             // ツリーからViewを検出する
    .perform(ViewAction)        // 検出されたViewを操作する
    .check(ViewAssertion)       // 検出されたViewを検証する
KotlinJava
    @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を検出する条件です。
ViewMatcherはViewを対象にしたHamcrestのMatcherで、条件にマッチしたViewが検出できます。

例えば、 withId(リソースID) は引数で指定したリソースIDを持つViewを検出します。

ViewAction
Viewを操作する方法です。
onViewで検出されたViewに対して、タッチスクリーン上を指で操作する事と同等な操作が行えます。

例えば、 click() は検出されたViewをクリックします。

ViewAssertion
Viewを検証する方法です。
onViewで検出されたViewにたいして、現在のViewの状態を検証します。

例えば、 maches(ViewMatcher) は「引数で指定した条件(ViewMatcher)に一致する状態であるか?」を調べます。

記述例で使ったViewMatcher/ViewAction/ViewAssertionは代表的なものです。その他、様々なものが使用できます。Espressoクイックリファレンスにまとめられているので参照してください。

スポンサーリンク

テストを行う(onData編)

「アプリの画面はツリー状にViewが配置されている」とonView編で述べました。しかし、このツリーに含まれないものがあります。それは、AdapterView(Spinner,ListViewなど)が管理し、リスト状に配置されるアイテムのViewです。

onDataはリスト構造からViewを検出します

このようなリスト構造からViewを検出してテスト(操作と検証)を行うのがonData関数(メソッド)です。

onDataの構文は下記の通りです。

onData(ObjectMatcher)           // リストからアイテムのViewを検出する
    .DataOptions                // onDataの補助条件(必要な場合のみ)
    .perform(ViewAction)        // 検出されたViewを操作する
    .check(ViewAssertion)       // 検出されたViewを検証する
KotlinJava
    @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の有無を調べる
    }
※ 記述例は画面にAdapterViewが一つだけ存在する場合です。複数ある場合はinAdapterView(ViewMatcher)を使って、対象にするAdapterViewを一つに定める必要があります。
※ inAdapterViewはDataOptionsです。

ObjectMatcher
アイテムのViewを検出する条件です。
ObjectMatcherはHamcrestのMatcherで、条件にマッチした要素を持つアイテムのViewが検出できます。

[ ArrayAdapterの例 ]

例えば、下記に示すようなArrayAdapterが設定されたAdapterViewだったとします。

KotlinJava
        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>
KotlinJava
        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)です。

DataOptions
アイテムのViewを検出する補助条件です。

例えば、 atPosition(Integer) はアイテムがインデックス位置のデータであることを指定します。引数はインデックス番号です。

例えば、 inAdapterView(ViewMatcher) はonDataが対象にするAdapterViewを指定します。引数はAdapterViewの条件を示すViewMatcherです。

ViewAction
Viewを操作する方法です。
onDataで検出されたViewに対して、タッチスクリーン上を指で操作する事と同等な操作が行えます。

例えば、 click() は検出されたViewをクリックします。

ViewAssertion
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はマッチします。全てマッチするならば条件は無駄です。

「昔はこのように書いていた。今は仕様が変更になった。」というだけかも知れません。

スポンサーリンク
スポンサーリンク