BETA

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

投稿日:2019-04-19
最終更新:2019-04-19
※この記事は外部サイト(https://qiita.com/terrierscript/items/21fe...)からのクロス投稿です

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を渡さないとデッドロックが起きる?のかうまく動かなかった

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

技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

@terrierの技術ブログ

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる
技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
or 外部アカウントではじめる
10秒で技術ブログが作れます!