fresh digitable

めんどくさかったなってことを振り返ったり振り返らなかったりするための記録

カジュアルにIdlingResourceを使う

  • 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が差し込まれるまで待つとかいうのも作れるようになるので興味のある方は挑戦してみてください。


最近読んだのでよかったらどうぞ。

peaks.cc

*1:結構すぐ返ってくるので待ってなくても割と成功する