AndroidのUIテストで、非同期処理が完了するまで待機する(Coroutine編)

AndroidのUIテストの話。Espresso前提。

非同期処理にCoroutineを使っている場合に、sleepなしで処理完了まで待機する方法を説明する。

Rxと違ってプロダクトコードの修正も必要。テスト実行時は、DispatchersのIO/Defaultをテスト用のものにすり替えられるようにする[^1]。そして、テスト実行時のDispatchersとIdlingResourceと紐付ける。

## Step1. CoroutineのDispatchersをすり替えられるようにする

以下のようなクラスを作る。[元ネタ。](https://github.com/DroidKaigi/conference-app-2019/blob/94a9955b319720cacfd28975ba9fb4698f043631/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2019/ext/CoroutinePlugin.kt)

```kotlin:CoroutinePlugin.kt
object CoroutinePlugin {

    private val defaultIoDispatcher: CoroutineContext = Dispatchers.IO
    val ioDispatcher: CoroutineContext
        get() = ioDispatcherHandler?.invoke(
                defaultIoDispatcher
        ) ?: defaultIoDispatcher

    @set:VisibleForTesting
    var ioDispatcherHandler: ((CoroutineContext) -> CoroutineContext)? = null

    private val defaultComputationDispatcher: CoroutineContext = Dispatchers.Default
    val defaultDispatcher: CoroutineContext
        get() = computationDispatcherHandler?.invoke(
                defaultComputationDispatcher
        )
                ?: defaultComputationDispatcher

    @set:VisibleForTesting
    var computationDispatcherHandler: ((CoroutineContext) -> CoroutineContext)? = null

    private val defaultMainDispatcher: CoroutineContext = Dispatchers.Main
    val mainDispatcher: CoroutineContext
        get() = mainDispatcherHandler?.invoke(
                defaultMainDispatcher
        ) ?: defaultMainDispatcher

    @set:VisibleForTesting
    var mainDispatcherHandler: ((CoroutineContext) -> CoroutineContext)? = null

    @VisibleForTesting
    @JvmStatic
    fun reset() {
        ioDispatcherHandler = null
        computationDispatcherHandler = null
        mainDispatcherHandler = null
    }
}
```

プロダクトコードからは、Dispatchers.IOやDefaultを直接使うのではなく、作ったCoroutinePlugin#ioDispatcherやdefaultDispatcherを参照する。

```kotlin
// withContext(Dispatchers.IO) { ... }
withContext(CoroutinePlugin.ioDispatcher) { ... }
```

## Step2. Coroutine処理待機用のTestRuleを作る

(Kotlin 1.4向けの記述なので、1.3系だとコンパイルエラーになる)

```kotlin
class CoroutineTestRule : TestRule {
    override fun apply(base: Statement, description: Description?): Statement = object : Statement() {
        override fun evaluate() {
            // IdlingThreadPoolExecutorないときはbuild.gradleに以下を記述する。
            // androidTestImplementation "androidx.test.espresso.idling:idling-concurrent:3.3.0"
            val executor = IdlingThreadPoolExecutor(
                    "Coroutine",
                    5,
                    10,
                    5L,
                    TimeUnit.SECONDS,
                    LinkedBlockingQueue()
            ) { r -> Thread(r) }

            CoroutinePlugin.ioDispatcherHandler =  { executor.asCoroutineDispatcher() }
            CoroutinePlugin.computationDispatcherHandler =  { executor.asCoroutineDispatcher() }

            try {
                base.evaluate() // この中でBefore/Test/Afterの処理が実行される
            } finally {
                CoroutinePlugin.reset()
                executor.shutdownNow()
            }
        }
    }
}
```

- IdlingThreadPoolExecutorを作る
  - EspressoのIdlingResourceとJavaのExecutorServiceの両方を実装しているクラス
  - IdlingRegistryのregister/unrgesiterは、内部で実行してくれる
- executorからCoroutineDispatcherを作り、CoroutinePluginのデフォルトのCoroutineDispatcherを差し替える

このTestRuleを使うと、Coroutineの非同期処理の完了までEspressoが待機するようになる。

## Step3. UIテストコードの記述

TestRuleをテストクラスに適用する。以下、例。

```kotlin
@LargeTest
@RunWith(AndroidJUnit4::class)
class FooTests {
    @get:Rule
    val coroutineTestRule = CoroutineTestRule()
    ...
}
```

[^1]: Coroutineでもメインスレッドで実行する処理は、何も対処しなくてもEspressoが待機してくれるはず。