AndroidのUIテストの話。Espresso前提。
非同期処理にCoroutineを使っている場合に、sleepなしで処理完了まで待機する方法を説明する。
Rxと違ってプロダクトコードの修正も必要。テスト実行時は、DispatchersのIO/Defaultをテスト用のものにすり替えられるようにする1。そして、テスト実行時のDispatchersとIdlingResourceと紐付ける。
Step1. CoroutineのDispatchersをすり替えられるようにする
以下のようなクラスを作る。元ネタ。
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を参照する。
// withContext(Dispatchers.IO) { ... }
withContext(CoroutinePlugin.ioDispatcher) { ... }
Step2. Coroutine処理待機用のTestRuleを作る
(Kotlin 1.4向けの記述なので、1.3系だとコンパイルエラーになる)
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をテストクラスに適用する。以下、例。
@LargeTest
@RunWith(AndroidJUnit4::class)
class FooTests {
@get:Rule
val coroutineTestRule = CoroutineTestRule()
...
}
-
Coroutineでもメインスレッドで実行する処理は、何も対処しなくてもEspressoが待機してくれるはず。 ↩