こんにちは、最近TDDに回帰している id:takuji31 です。
最近 Espresso Idling Resource を使い始めたので紹介します。
なお、この記事の一部は本日開催の関西モバイルアプリ研究会#24で発表された内容です。
UIテストを書く時の悩み
- 何かが切り替わる時にUIのアニメーションが発生して、その後表示が切り替わるようなものをテストしたい
- APIからデーターを非同期に取得して、画面に表示される値が正しいかテストしたい
↑のようなテストをする時、 “テストが可能になるまで待つ” という動作を実装するのは結構面倒です。
雑な解決方法
面倒なのでよくやるのは Thread.sleep(1000)
みたいな感じで十分な時間待つことですね。
ですがこの対処方法には「待ったところで実は終わってないかもしれない = テストがたまにコケる」 という問題があります。
ちょっと賢い方法
われわれはかしこいので、RxJavaなんかを使って完了を待つObservableを作ったり、完了を待ち受けるListenerを作ったりするかもしれません。
これは確実に完了まで待つことができますが
- 複数のコンポーネントの完了を全て待つ時の実装が複雑になりがち
- そもそもテストのためにそんなものを手で時間をかけて実装するのは本質からそれているのではないか
といった問題があります。
Espresso Idling Resource
↑のような問題を解決するのが Espresso Idling Resource
です。
これはEspressoのプラグインのようなもので、アイドリングの仕組みを提供してくれます。
このライブラリーで用意されている IdlingRegistry
に自前の IdlingResource
を登録すれば、そのリソースがアイドリング状態になるまで、テストが先に進むのを止めてくれます。
使い方
セットアップ
build.gradleに設定
compile 'com.android.support.test.espresso:espresso-idling-resource:3.0.0'
IdlingResource
まずは IdlingResource
を作ります。
fun getName(): String
このIdlingResourceの名前を返します。この名前はロギングや二重登録のチェックに使われるようです。
fun isIdleNow(): Boolean
このリソースがアイドル状態かどうかを返します。
fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?)
アイドル状態になった時に実行するCallbackを登録します。
callbackはnullableのようです。
実装
ここではめちゃくちゃ簡単にしたいので、propertyにBooleanの値をセットするだけのものにします。
class SimpleIdlingResource(private val name: String) : IdlingResource { private var callback: IdlingResource.ResourceCallback? = null var isIdle: Boolean = false set(value) { field = value if (value) { callback?.run { onTransitionToIdle() } } } override fun getName(): String = this.name override fun isIdleNow(): Boolean = isIdle override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) { this.callback = callback } }
使う
アプリ側
アプリ側で SimpleIdlingResource
を使うコードを書きましょう
class ViewModel { var artists: List<Artist> = emptyList() var loadingDisposable: Disposable? = null @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) val loadingIdlingResource : SimpleIdlingResource = SimpleIdlingResource("IdlingResourceViewModelLoading") fun onCreate() { // fake API request fakeReload() } private fun fakeReload() { loadingDisposable = Single .timer(3, TimeUnit.SECONDS) .doOnSubscribe { loadingIdlingResource.isIdle = false } .doFinally { loadingIdlingResource.isIdle = true } .subscribe { _, _ -> artists = Artist.list } } fun onDestroy() { loadingDisposable?.run { dispose() loadingDisposable = null } } }
APIを叩いてデーターを取得したような感じで3秒待ってリストを更新する処理を書きました。
処理の開始と終了時に loadingIdlingResource.isIdle
の値を更新しています。
テストコード
テストでActivityを起動する前に IdlingRegistry
に IdlingResource
を登録します。
@Before
なメソッドでやってやるのがよいでしょう。
class IdlingResourceActivityTest { @JvmField @Rule val rule : IntentsTestRule<IdlingResourceActivity> = IntentsTestRule(IdlingResourceActivity::class.java, true, true) var initializeIdlingResource : IdlingResource? = null @Before fun setUp() { initializeIdlingResource = rule.activity.viewModel.loadingIdlingResource IdlingRegistry.getInstance().register(initializeIdlingResource) } // ... }
テスト後には IdlingRegistry
から IdlingResource
を削除してやります。
@After
なメソッドでやってやるのがよいでしょう。
class IdlingResourceActivityTest { // ... @After fun tearDown() { initializeIdlingResource?.run { IdlingRegistry.getInstance().unregister(this) initializeIdlingResource = null } } }
テストは普通にEspressoで書きます。
class IdlingResourceActivityTest { @Test fun testReload() { // remove 3 items repeat(3) { openActionBarOverflowOrOptionsMenu(rule.activity) onView(withText("Remove first")).perform(click()) } onView(withId(R.id.swipeRefreshLayout)).perform(swipeDown()) onView(withId(R.id.recyclerView)).check { view, _ -> val recyclerView = view as RecyclerView assertEquals(recyclerView.childCount, Artist.list.size) assertEquals(recyclerView.adapter.itemCount, Artist.list.size) } } }
用意されているMatcherやViewActionを使わない場合は Espresso.onIdle()
を呼ぶとアイドル状態になるまで同期的に待ってくれます。
デフォルトで用意されているIdlingResource
CountingIdlingResource
セマフォっぽいやつ、 increment()/decrement()
を呼んでカウントが増減し、0ならアイドルになる、1以上ならアイドルじゃないとなるようです。
CountingIdlingResource | Android Developers
IdlingScheduledThreadPoolExecutor
/ IdlingThreadPoolExecutor
ThreadPoolExecutor、IdlingScheduledThreadPoolExecutor
は遅延実行できる。
IdlingScheduledThreadPoolExecutor | Android Developers
UriIdlingResource
UriをキーにできるIdlingResource、ドキュメントを見た感じWebViewと使うことを想定してるっぽい?
UriIdlingResource | Android Developers
ハマりどころ
Proguardを有効にした状態でテストを実行すると、IdlingRegistryがなくてコケる。
Proguardの設定を書きましょう。
-keep class android.support.test.espresso.** { public *; }
最後に
割と簡単に実装できるので、Thread.sleepやめてIdlingResource使いましょう。