💯

TestDispatcher完全に理解した

2024/08/03に公開

TL;DR

  • プロダクションコードのDispatcherはとりあえず全部DIにしとこう
  • 直列処理をテストしたいとき:runTest(UnconfinedTestDispatcher())
  • 並列処理をテストしたいとき:runTest + StandardTestDispatcherの注入

説明

DispatcherはDIしよう

class DispatcherModule {
    fun provideIODispatcher: CoroutineDispatcher = Dispatchers.IO
}

class HogeRepository(
    private val dispatcher: CoroutineDispatcher,
) {
    suspend fun fetch() = withContext(dispatcher) {
        // データ取得処理
    }
}

※DIの書き方は使用するDIライブラリによります。上記は雰囲気実装です。

Android Developersには「CoroutineContextをDIするともっと柔軟になるのでよいよ」と書いてあったのですが、そこまでの必要性に私は直面したことがないので、まあCoroutineDispatcherのDIで十分かなと思ってます。

but you can also inject the broader CoroutineContext type, which allows for even more flexibility during tests.

直列処理のテスト

class HogeRepositoryTest {
    private lateinit var sut: HogeRepository

    @BeforeTest
    fun setUp() {
        sut = HogeRepository(
            dispatcher = Dispatchers.IO // ここに入れるDispatcherはなんでもいい
        )
    }

    @Test
    fun testFetch() = runTest(UnconfinedTestDispatcher()) {
        val actual = sut.fetch()

        // 検証
    }
}

UnconfinedTestDispatcherは、その内部でのスレッド切り替えを抑制するような働きをするため、runTestに渡してしまえば、dispatcherのDI部分を気にしなくてよくなります。

並列処理のテスト

class HogeRepositoryTest {
    @Test
    fun testFetch() = runTest {
        val sut = HogeRepository(
            dispatcher = StandardTestDispatcher(testScheduler),
        )

        val actual = sut.fetch()

        // 検証
    }
}

UnconfinedTestDispatcherは実際のスレッド管理に忠実ではないので、並列処理のテストをやらせるべきではありません。

そこで、並列処理のテストではStandardTestDispatcherを使用します。runTestの第一引数を省略するとStandardTestDispatcherが使用されます。

ただし、StandardTestDispatcherはスレッド切り替えを抑制しないので、DI部分からもStandardTestDispatcherを注入してやる必要があります。そうしないと、テストが不安定になってしまいます。

おわりに

この記事は、Android Developersに書いてあることを自分なりに解釈したものです。

https://developer.android.com/kotlin/coroutines/test

もし間違い等あったらコメントいただけると助かります!

Discussion