- UIテストをやる時にMockWebServerを使ってサーバの挙動をモックしていると、レスポンスをちゃんと待ち構えないとテストが失敗することが稀によくある*1
- カジュアルに
IdlingResource
を使ってカジュアルに待てるようにしたい - テストを書くためのハードルを下げたい
ポイント:
- ある状態になるまで待ちたくなった時にregisterして所望の状態になったらすぐにunregisterする。そうせず大域的に使うといろんな条件がバッティングして失敗する。
- registerしたらなにがなんでもunregisterされるようにしたいのでtry (catch) finallyでくくる
isIdleNow()
のなかでActivityLifecycleMonitor.getActivitiesInStage(Stage)
を使ってactivityを取得できれば勝確ActivityScenario
とかActivityTestRule
とかをわたしてもよいかも?- 結構無茶なことをしているし
IdlingThreadPoolExecutor
とかを使ってうまくやるのがいいと思う - ダイアログとか
RecyclerView
の状態を待ちたいときはこうするしかないような気もするがそもそもこんなことしなければならないのが間違っている気がする
コード例:
まずはIdlingResources
。結局はisIdleNow()
の挙動を実装できればいい。公式サイトにはisIdleNow()
の中でIdlingResource.ResourceCallback.onTransactionToIdle()
を呼ぶなと書いてあるが今回の用途ではここで呼ばないとテストが進まなくなることがあるので仕方なく呼ぶ。
fun createIdlingResource(name: String, block: () -> Boolean): IdlingResource { return object : IdlingResource { override fun getName() = name override fun isIdleNow(): Boolean { val isIdle = block() if (isIdle) { callback?.onTransitionToIdle() } return isIdle } private var callback: IdlingResource.ResourceCallback? = null override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) { this.callback = callback } } } fun waitWithIdlingResource(name: String, block: () -> Boolean, afterTask: () -> Unit) { val idlingRegistry = IdlingRegistry.getInstance() val resource = createIdlingResource(name, block) try { idlingRegistry.register(resource) afterTask() } finally { idlingRegistry.unregister(resource) } }
先ほどのメソッドを作ってテスト対象のActivityが所定のStageにIdlingResourceを作ってみる。
inline fun <reified T : Activity> waitForActivity(stage: Stage, noinline afterTask: () -> Unit) { val name = "wait_for_${T::class.java.simpleName}_in_${stage.name}" waitWithIdlingResource(name, { ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(stage).firstOrNull { it is T } != null }, afterTask) }
先ほどのwaitForActivityをリファクタリングしてFragmentが差し込まれるまで待つIdlingResourceを作ってみる。
inline fun <reified T : Activity> waitForActivity( stage: Stage = Stage.RESUMED, name: String = "wait_for_${T::class.java.simpleName}_in_${stage.name}", crossinline onActivity: (T) -> Boolean = { true }, noinline afterTask: () -> Unit ) { waitWithIdlingResource(name, { val a = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(stage).firstOrNull { it is T } ?: return@waitWithIdlingResource false return@waitWithIdlingResource onActivity(a as T) }, afterTask) } inline fun <reified T : Fragment> waitForFragment(noinline afterTask: () -> Unit) { waitForActivity<FragmentActivity>(name = "wait_for_${T::class.java.simpleName}", onActivity = { a -> a.supportFragmentManager.fragments.firstOrNull { it is T } != null }, afterTask = afterTask) }
しなくていいならしないに越したことはないと思うけど、退っ引きならない事情でどうしてもやらなければならない時だけどうぞ。Activityを待つ奴を応用するとRecyclerViewに所望のViewが差し込まれるまで待つとかいうのも作れるようになるので興味のある方は挑戦してみてください。
最近読んだのでよかったらどうぞ。
*1:結構すぐ返ってくるので待ってなくても割と成功する