Kotlin coroutines 1.6.0-RCでリワークされたkotlinx-coroutines-testを見てみる

この記事は[and factory.inc Advent Calendar 2021](https://qiita.com/advent-calendar/2021/andfactory) 1日目の記事です。

Kotlin coroutines 1.6.0-RCがリリースされ、テスト周りに大幅な変更が入ったので確認してみました。

※kotlinx-coroutines-testのバージョン1.6.0-RCで検証を行っています。
ほとんどのAPIがExperimentalなので将来的に変更される可能性もあります。

追記（2022/5/26）
Android Developer向けのドキュメントが非常にわかりやすいのでこちらもご確認ください。

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


# Dispatchers.setMain()
`Dispatchers.Main`が使われている箇所を任意のDispatcherに置き換える関数です。
これ自体はもともと存在していましたが、セットするDispatcherを変更する必要があります。


``` Before.kt
class SomeTest {
    val dispatcher = TestCoroutineDispatcher()

    @Before
    fun setUp() {
        Dispatchers.setMain(dispatcher)
    }

    @After 
    fun tearDown() {
        Dispatchers.resetMain()
    }

    @Test
    fun testA() = dispatcher.runBlockingTest {
        // TODO
    }
}
```


``` After.kt
class SomeTest {

    @Before
    fun setUp() {
        Dispatchers.setMain(StandardTestDispatcher())
    }

    @After 
    fun tearDown() {
        Dispatchers.resetMain()
    }

    @Test
    fun testA() = runTest {
        // TODO
    }
}
```

`setMain()`で`StandardTestDispatcher()`をセットして`runTest()`でテストを実行します。
これらの説明は後述します。

# TestCoroutineScheduler
テストで使用されるコルーチンのスケジューラで、自動的に`delay()`等の遅延がスキップされます。
後述する`TestDispatcher`はこのスケジューラによってパラメータ化されており、複数のDispatcherが同じスケジューラを共有できます。
Dispatcherはイベントのスケジューリングが必要な場合、スケジューラに通知してスケジューラがタスクの順序を決定する仕組みになっています。

時間を進める`advanceTimeBy`、必要に応じて時間を進めながらタスクを実行する`advanceUntilIdle`、現段階までで実行が予定されているタスクを実行する`runCurrent`などのスケジュールをコントロールする関数が用意されています。

以前は`DelayController`というinterfaceを用いて時間のコントロールを行っていましたが、こちらは非推奨となります。

# TestScope
`TestCoroutineScope`が非推奨となり、`TestScope`が追加されました。
後述する`runTest`に統合されています。（`TestScope()`関数でインスタンスを生成することも可能です。）
`coroutineContext `に`TestCoroutineScheduler`が含まれていており、取得することが可能です。

``` TestScope.kt
val scope = TestScope()
val scheduler = scope.testScheduler
```

# runTest
`runBlockingTest`が非推奨となり、`runTest`が追加されました。
コルーチンを含むコードをテストするのに用い、suspend関数を実行できます。


`runTest`内で子コルーチンを起動した場合、別のCoroutineDispatcherを渡していない限り単一スレッドで実行されるのでテスト本体と並行で実行されることはありません。
子コルーチンを実行するためにはテスト本体を一時停止させるか、`TestCoroutineScheduler`を利用してスケジューリングを制御する必要があります。
以下のコードのコメントに書かれている数字は実行される順番を示します。

``` RunTest.kt
@Test
fun exampleTest() = runTest { // this: TestScope
    // 1
    val job = launch {
        // 3
    }
    // 2
    job.join() // テスト本体がここで中断されて子コルーチンが実行される
    // or this.advanceUntilIdle(), this.runCurrent()
    // 4
}
```

`Dispatchers.setMain()`を使用してDispatcherを`TestDispatcher`に置き換えた場合も同様の考慮が必要です。
以下のコードではSomeClassで実行するコルーチンのDispatcherを`StandardTestDispatcher`に置き換えています。
この場合も子コルーチンと同様に並行で実行されることはないので、スケジュールを制御する必要があります。

``` RunTest.kt
class SomeTest {
    @Before
    fun setUp() {
        Dispatchers.setMain(StandardTestDispatcher())
    }

    @Test
    fun exampleTest() = runTest {
        val some = SomeClass()
        advanceUntilIdle() // これを実行することでSomeClassのコルーチンが実行される
        assertEquals(some.flag, true)
    }
}


class SomeClass {
    var flag = false
        private set

    init {
        CoroutineScope(Dispatchers.Main).launch {
            flag = true
        }
    }
}
```


# TestDispatcher
`TestCoroutineDispatcher`が非推奨となり、`StandardTestDispatcher()`と`UnconfinedTestDispatcher()`2つのDispatcherが導入されました。

## StandardTestDispatcher
`TestCoroutineScheduler`にリンクされている以外に特別な動作はしないシンプルなDispatcherです。
`runTest`や`TestScope`でDispatcherを指定せずにコルーチンを実行した場合、デフォルトで用いられます。


`runTest`内では子コルーチンが直ちに実行されないという話をしましたが、それはこのDispatcherの特性によるものです。
launch, asyncブロックは`CoroutineStart.UNDISPATCHED`でパラメータ化されていない限りすぐには実行されないので注意が必要です。

## UnconfinedTestDispatcher
`Dispatchers.Unconfined`に似た動作をするDispatcherです。
（複数のコルーチンがこのDispatcherにキューイングされているとき、実行順序を保証しない）
`StandardTestDispatcher`と同様に`TestCoroutineScheduler`にリンクされています。

このDispatcherを用いて`runTest`内で子コルーチンを起動した場合（launch, async）は直ちに実行されます。
例えば、StateFlowやChannelをテストしたい場合に便利です。

以下のコードでは、StateFlowのサブスクライブに`UnconfinedTestDispatcher`を利用しています。
子コルーチンが直ちに実行されることでStateFlowの値を変化させる前にcollectが動作し、すべての値を観測することができます。

``` FlowTest.kt
class SomeTest {
    @Before
    fun setUp() {
        Dispatchers.setMain(StandardTestDispatcher())
    }

    @Test
    fun exampleTest() = = runTest {
        val values = mutableListOf<Int>()
        val stateFlow = MutableStateFlow(0)
        val job = launch(UnconfinedTestDispatcher(testScheduler)) {
            stateFlow.collect {
                values.add(it)
            }
        }
        stateFlow.value = 1
        stateFlow.value = 2
        stateFlow.value = 3
        job.cancel()
        assertEquals(listOf(0, 1, 2, 3), values)
   }
}

```

# Migration
1.6.0-RC以前のAPIの多くは非推奨となっており、将来的に削除される予定なので新しいAPIに移行する必要があります。
既存のテストコードをリプレイスする際は[マイグレーションガイド](https://github.com/Kotlin/kotlinx.coroutines/blob/1.6.0-RC/kotlinx-coroutines-test/MIGRATION.md)が用意されているので、規模によってはステップバイステップで切り替えていくのも良いかと思います。

# 参考

https://github.com/Kotlin/kotlinx.coroutines/tree/1.6.0-RC/kotlinx-coroutines-test

https://github.com/Kotlin/kotlinx.coroutines/blob/1.6.0-RC/kotlinx-coroutines-test/MIGRATION.md