💯

TestDispatcher完全に理解した

に公開

TL;DR

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

説明

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を注入してやる必要があります。そうしないと、テストが不安定になってしまいます。

ViewModelのテスト

class MainDispatcherRule(
    val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
    override fun starting(description: Description) {
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description) {
        Dispatchers.resetMain()
    }
}

class HogeViewModelTest {
    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    @Test
    fun testXxx() = runTest {
        val sut = HogeViewModel()

        val actual = sut.xxx()

        // 検証
    }
}

viewModelScopeは内部でMainディスパッチャを使おうとするため、setMainでテストディスパッチャに固定しましょう。

おわりに

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

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

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

Discussion