動的な View 表示時の Espresso テスト TIPS

動的な View 表示時の Espresso テスト TIPS

こんにちは。Android / iOS エンジニアの原田です。

今回は Android ONE で実際に実行している Espresso を使った UI テストの Tips を紹介しようと思います。

はじめに

Espresso は簡潔に UI テストを書ける非常に強力なライブラリです。しかし、実際に稼働しているプロダクトは非同期 かつ 動的に UI が変わるのが普通です。公式 Docs を読んだだけでは挙動が複雑な View に対して、どのようにテストを書けばいいか戸惑うことも多いかと思います。

今回はそういったエンジニアに向けて、Espresso の Tips をまとめてみました。

対象者

動的に表示内容が変わる View の Espresso テストを書くエンジニア

実行環境

Android Studio Chipmunk | 2021.2.1 patch 1

Test Libraries

2022/6/30時点の Stable Releaseを採用

  • androidx.test:rules:1.4.0
  • androidx.test.espresso:espresso-contrib:3.4.0
  • androidx.test.uiautomator:uiautomator:2.2.0

Tips of Espresso Test

1. 非同期に更新される View の場合

テスト実行時に、任意の非同期処理でテスト対象の view の表示を待つ必要がある場合、下記のようなメソッドを準備しておくと便利です。

private val uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())

/**
 * @param selector テスト対象の view の Selector
 * @param timeoutMilliSec タイムアウト
 */
fun waitUntilHasObject(selector: BySelector, timeoutMilliSec: Long = 10000) =
    ViewMatchers.assertThat(
        // selector に合致する view が表示されるまで待機
        uiDevice.wait(
            Until.hasObject(selector),
            timeoutMilliSec
        ),
        Matchers.`is`(true)
    )
Utility Method
@Test
fun displayAsyncViewTest() {
    // 「test view」と書かれた view が表示されたら success
    // タイムアウトの場合 failed
    waitUntilHasObject(By.text("test view")
}
Sample Test Code

2. RecyclerView 内の View の場合

下記のように、テスト対象の view が RecyclerView 上の画面外にある場合を考えます。

image

この場合のテストをするためには、画面内にテスト対象の view が表示されるようにスクロールしてあげる必要があります。

@Test
fun viewInRecyclerViewTest() {
	val recyclerView = Espresso.onView(
		ViewMatchers.withId(R.id.recyclerView)
	)

    recyclerView.perform(
        // targetView までスクロール
        RecyclerViewActions.scrollTo<GroupieViewHolder>(   
            ViewMatchers.hasDescendant(ViewMatchers.withId(R.id.targetView))
        )
    )
		
    // targetView をタップ
    Espresso.onView(
        ViewMatchers.withId(R.id.targetView)
    ).perform(ViewActions.click())
}
Sample Test Code

3. ViewPager 内の RecyclerView

今度は下記のようにViewPager 内の RecyclerView へテストをしたいとします。

なお、ここではより複雑なケースの知見を共有するために、RecyclerView 同士の ID が Fragment 内で重複しているものとします。

image

まずは、目的の RecyclerView を取得するために、下記のような Matcher を用意してあげます。

/**
 * @param viewPagerMather テスト対象の viewPager の Matcher
 * @param index テスト対象の item の index
 * @return Matcher
 */
fun existItemInViewPager(viewPagerMather: Matcher<View>, index: Int): Matcher<View> {
    return object : TypeSafeMatcher<View>() {
        override fun matchesSafely(item: View): Boolean {
            val pager = item.parent
            return if (pager is ViewGroup) {
                // Mathcer に合致する かつ viewPager の指定 index の item と一致する
                viewPagerMather.matches(pager) &&
                    pager.getChildAt(index).equals(item)
                } else {
                    viewPagerMather.matches(pager)
                }
            }
        }

        override fun describeTo(description: Description) {
        }
    }
}
Utility Method

上記を用いて、RecyclerView の ID × ViewPager の ID × index で一意に RecyclerView を Match させます。

@Test
fun viewInRecyclerViewOnViewPagerTest() {
    val recyclerView = Espresso.onView( 
        Matchers.allOf(
            // テスト対象の RecyclerView の ID
            ViewMatchers.withId(R.id.recyclerView),
            // 条件に合致する RecyclerView が ViewPager 内に存在するか
            ViewMatchers.isDescendantOfA(
                existItemInViewPager(
                    ViewMatchers.withId(R.id.viewpager),
                    2
                )
            )
        )
    )

    // 2. の要領で recyclerView の targetView に対して任意テストを実行
}
Sample Test Code

最後に

プロダクトを作り込む際には、より洗練された操作感やデザインを実現するために、View の構造は複雑になりがちです。特に、RecyclerView や ViewPager は Android 開発において頻出の概念かと思います。

今回はそれらが用いられた画面に対して、Espresso テストを作成する時の参考になれば幸いです。