kotlinx-coroutines-testのDispatchers.setMainについて
Coroutinesの単体テストの話です、マルチスレッドな関数の単体テストを書いていると理解が曖昧なところがまだあるので改めて調べることにします。趣旨としてはCoroutineのテストを書くときに@BeforeでDispatchers.setMainを何故やる必要があるのかと何が行われているのかです。長くなりそうなので一旦記事にします。
TL;DL
- Androidコンテキストを排除してテストをするためにsetMainでDispatcherを差し替える必要がある
- 利用するMainDispatcherはloadPriorityで決まる
環境
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2"
背景
CoroutineのDispatchers.Main
は内部的にgetMainLooper()を呼び出している[1]。Looperはandroid.osパッケージに含まれており、UIスレッドで起動することを考えれば当たり前ではあるのだが、つまるところ単体テストではDispatchers.Main
は使えない。なのでviewModelScope
やlaunch(context = Dispatchers.Main)
のようにCoroutineを起動する処理のテストでは何らかの方法でDispatchers.Main
を差し替える必要がある。この問題に対処するためにkotlinx-coroutines-test
ではDispatchers.Mainの中身を差し替えられるようになっている。
setMain
setMainで行う処理は以下のようになっておりシンプル
TestDispatchers.kt[2]
@ExperimentalCoroutinesApi public fun Dispatchers.setMain(dispatcher: CoroutineDispatcher) { require(dispatcher !is TestMainDispatcher) { "Dispatchers.setMain(Dispatchers.Main) is prohibited, probably Dispatchers.resetMain() should be used instead" } val mainDispatcher = Dispatchers.Main require(mainDispatcher is TestMainDispatcher) { "TestMainDispatcher is not set as main dispatcher, have $mainDispatcher instead." } mainDispatcher.setDispatcher(dispatcher) }
mainDispatcherに対してTestMainDispatcherの型チェックが入っている、なのでテストではTestDispatcherを使っていることになるのだが、mainDispatcherの型がMainCoroutineDispatcherにしか見えないのでまずはここを追いかけることにする。
Dispatchers.kt[3]
@JvmStatic public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
MainDispatcherLoader.kt[4]
val factories = if (FAST_SERVICE_LOADER_ENABLED) { FastServiceLoader.loadMainDispatcherFactory() } else { // We are explicitly using the // `ServiceLoader.load(MyClass::class.java, MyClass::class.java.classLoader).iterator()` // form of the ServiceLoader call to enable R8 optimization when compiled on Android. ServiceLoader.load( MainDispatcherFactory::class.java, MainDispatcherFactory::class.java.classLoader ).iterator().asSequence().toList() }
FastServiceLoader.kt[5]
private fun <S> load(service: Class<S>, loader: ClassLoader): List<S> { return try { loadProviders(service, loader) } catch (e: Throwable) { // Fallback to default service loader ServiceLoader.load(service, loader).toList() } }
MainDispatcherLoader.kt[4]
factories.maxBy { it.loadPriority }?.tryCreateDispatcher(factories)
ということでloadPriorityが最大のMainDispatcherFactoryでMainCoroutineDispatcherを生成することになる。factoriesの中身はFastServiceLoaderで設定されていてTestMainDispatcherFactory
と AndroidDispatcherFactory
である。それぞれのpriorityは
TestMainDispatcherFactory[6]
internal class TestMainDispatcherFactory : MainDispatcherFactory { override val loadPriority: Int get() = Int.MAX_VALUE }
AndroidDispatcherFactory[1]
internal class AndroidDispatcherFactory : MainDispatcherFactory { override val loadPriority: Int get() = Int.MAX_VALUE / 2 }
となっておりテスト時にはTestMainDispatcher
が使用される。
参考
[2] TestDispatchers
[3] Dispatchers