LoginSignup
25
6

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-11-30

この記事はand factory.inc Advent Calendar 2021 1日目の記事です。

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

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

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

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です。
runTestTestScopeで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に移行する必要があります。
既存のテストコードをリプレイスする際はマイグレーションガイドが用意されているので、規模によってはステップバイステップで切り替えていくのも良いかと思います。

参考

25
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
6