概要
Kotlin CoroutineはDispatcherによって実行スレッドを制御する。そのDispatcherを切り替えることで、コルーチンのコードをテスト可能にできます。
DroidKaigiではCoroutinePluginによるやり方を作成して利用していました。そこにAppCoroutineDispatcherを使うPRが来ました。
3つの指定方法があるのでそれぞれ見ていきましょう。
CoroutinePlugin
AppCoroutineDispatcher
kotlinx-coroutines-test
CoroutinePluginによるやり方
CoroutinePluginはKotlin Corotuinesに存在するクラスではなく、アプリ内で作って使う形になります。
DroidKaigiアプリで採用されている方法
RxJavaのスケジューラーを真似たやり方でstaticでもって置いて切り替えができるようにする方式です。
object CoroutinePlugin {
private val defaultIoDispatcher: CoroutineContext = Dispatchers.IO
val ioDispatcher: CoroutineContext
get() = ioDispatcherHandler?.invoke(
defaultIoDispatcher
) ?: defaultIoDispatcher
@set:VisibleForTesting
var ioDispatcherHandler: ((CoroutineContext) -> CoroutineContext)? = null
...
テストでは CoroutinePlugin.mainDispatcherHandler = { Dispatchers.Default }
などで置き換えが出来ます。
AppCoroutineDispatchersによるやり方
AppCoroutineDispatchersはこういうクラスを作って、これをInjectすることで置き換えるパターン。Chris Banesがやっていたものになります
data class AppCoroutineDispatchers(
val database: CoroutineDispatcher,
val disk: CoroutineDispatcher,
val network: CoroutineDispatcher,
val main: CoroutineDispatcher
)
@Singleton
@Provides
fun provideDispatchers(schedulers: AppRxSchedulers) =
AppCoroutineDispatchers(
database = schedulers.database.asCoroutineDispatcher(),
disk = schedulers.disk.asCoroutineDispatcher(),
network = schedulers.network.asCoroutineDispatcher(),
main = UI
)
kotlinx-coroutines-testによるやり方
kotlinx-coroutines-testを依存関係に追加して、これだけです。
class SomeTest {
private val mainThreadSurrogate = newSingleThreadContext("UI thread")
@Before
fun setUp() {
Dispatchers.setMain(mainThreadSurrogate)
}
@After
fun tearDown() {
Dispatchers.resetMain() // reset main dispatcher to the original Main dispatcher
mainThreadSurrogate.close()
}
@Test
fun testSomeUI() = runBlocking {
launch(Dispatchers.Main) { // Will be launched in the mainThreadSurrogate dispatcher
...
}
}
}
これには一つだけ問題点があって、Dispatchers.Mainしか置き換えが出来ません。
どうしたか
CoroutinePluginによるやり方を使いました。
メリデメに関しては以下にまとまっています
https://github.com/DroidKaigi/conference-app-2019/issues/236
まずkotlinx-coroutines-test
はMainしか置き換えできないので、用途に合わない場合も出てきそうでした。ただ今考えてみるとこれとrunBlocking()
でテストを対応できた説もあるので、チャレンジしてみるのはいいと思います。
AppCoroutineDispatchersによるやり方はDroidKaigiではReceiveChannel.toLiveData()
にわたす必要があるなど、大量に使われる部分でもDispatcherを渡していく必要が出てきたりなどするため、結構煩雑になってしまいそうでした。そのためCoroutinePluginを使う方法をそのまま使いました。
好みの問題もあるので、基本的にはkotlinx-coroutines-test
でやってみて、何か問題があれば変えていくのがいいかなと思います。