sky’s 雑記

主にAndroidとサーバーサイドの技術について記事を書きます

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は使えない。なのでviewModelScopelaunch(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で設定されていてTestMainDispatcherFactoryAndroidDispatcherFactory である。それぞれの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が使用される。

参考

[1] HandlerDispatcher

[2] TestDispatchers

[3] Dispatchers

[4] MainDispatcherLoader

[5] FastServiceLoader

[6] TestMainDispatcherFactory