Help us understand the problem. What is going on with this article?

TestCoroutineContextを使ってdelayやtimeoutの絡むCoroutineのテストをする

More than 1 year has passed since last update.

coroutineでタイムアウトを取り扱うようなテストをしようとした時に詰まったのでメモ

準備

今回はざっくりこんなコードがあった場合を考える

class Sample {
    // テストしたいコード
    suspend fun methodWithTimeout(suspendItem: SuspendItem): Int?{
        return withTimeoutOrNull(100L){
            suspendItem.someAsync()
        }
    }
}

class SuspendItem {
    // ランダムにdelayして値を返すような関数を考える。テスト時はこちらをmockすることを考える
    suspend fun someAsync(): Int {
        delay(Random.nextLong(10000L))
        return 10
    }
}

テストを書く

mockには今回mockito-kotlinを利用している。
正常系を考える場合は下記のように特に考えずにmockすれば良い

@Test
fun methodWithMock() {
    val s = mock<SuspendItem>{
        onBlocking {
            someAsync()
        } doReturn 10
    }
    runBlocking {
        val result = Sample().methodWithTimeout(s)
        assertEquals(10, result)
    }
}

そしてタイムアウトを考える場合、Thread.sleepなどを考えたくなるがこれはうまくいかない

// 駄目なケース
@Test
fun methodWithMock_invalid() {
    val s = mock<SuspendItem>{
        onBlocking {
            someAsync()
        } doAnswer {
            Thread.sleep(10000)
            10
        }
    }
    runBlocking {
        val result = Sample().methodWithTimeout(s)
        assertEquals(null,result) // 通らない!
    }

}

解法:TestCoroutineContextを利用する

どうすれば良いのか色々調べるとTestCoroutineContextを利用すれば良いと判明した

こんな感じになる

@Test
fun methodWithMock_withTestCoroutineContext() {
    val context = TestCoroutineContext()
    runBlocking(context) {
        val job = GlobalScope.launch(context) {
            val s = mock<SuspendItem> {
                onBlocking {
                    someAsync()
                } doAnswer {
                    context.advanceTimeBy(10L) // 時間を進める
                    10
                }
            }
            val result = Sample().methodWithTimeout(s)
            assertEquals(10, result)
        }
        job.join()
        assertEquals(true, job.isCompleted)
    }
}
@Test
fun methodWithMock_withTestCoroutineContext_timeout() {
    val context = TestCoroutineContext()
    runBlocking(context) {
        val job = GlobalScope.launch(context) {
            val s = mock<SuspendItem> {
                onBlocking {
                    someAsync()
                } doAnswer {
                    context.advanceTimeBy(20000000L) // タイムアウトするぐらい時間を進める
                    10
                }
            }
            val result = Sample().methodWithTimeout(s)
            assertEquals(null, result) // Timeoutしてnullが返ってきた!
        }
        job.join() // joinを忘れるとテストが走らずにpassしてしまうことに注意
        assertEquals(true, job.isCompleted)
    }
}

ひとまずこれで十分に動いたが、下記などは理解が浅いので未解決な箇所として残っている。

  • GlobalScope.launch以外のscopeなどでは上手く動かすことができなかった。
  • runBlockingcontextを渡さないとデッドロックが起きる?のかうまく動かなかった

おそらくもう少しシンプルに書く手法がありそうだが、今回は切り上げた。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした