kotlinx-coroutines-testのrunBlockingTestについて

前回に引き続きCoroutinesの単体テストの話です。

[https://iwsksky.hatenablog.com/entry/2020/12/09/014603:title]

今回はrunBlockingTest[1]について取り扱いたいと思います。

⚠理解が曖昧な状態で記述している可能性があります、間違いがあれば訂正お願いします。

## 環境
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.20"

testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.9"

## runBlockingTestとは
kotlinx-coroutines-testに含まれるAPIでrunBlocking同様に現在のスレッドをブロックしてコルーチンを起動することが可能。

```kotlin
@ExperimentalTime
@Test
fun exampleTest() = runBlocking {
   val deferred = async {
       println("SEC1 is " + measureTime { delay(10_000) })
       println("SEC2 is " + measureTime { async { delay(10_000) }.await()})
   }
   deferred.await()
}

// sec1 is 10.0s
// sec2 is 10.0s
```
上記例ではrunBlockingに渡しているブロックで2度コルーチンが起動されておりそれぞれで10secディレイを入れているため以下のような出力になる。

次にrunBlockingをrunBlockingTestに変更した場合

```kotlin
ExperimentalTime
@Test
fun exampleTest() = runBlockingTest {
   val deferred = async {
       println("SEC1 is " + measureTime { delay(10_000) })
       println("SEC2 is " + measureTime { async { delay(10_000) }.await()})
   }
   deferred.await()
}

// sec1 is 8.78ms
// sec2 is 1.51ms
```
delayが即時実行され処理が完了していることがわかる。

## runBlockingTestの用途
runBlockingTestが導入された背景はTesting Coroutines on Android (Android Dev Summit '19)[2]とKotlinConf 2019: Testing with Coroutines by Sean McQuillan[3]が詳しい。Android Dev Summitでは良い単体テストは速く(fast)、信頼性があり(reliable)、独立している(isolated)と言われており、runBlockingTestは特に速さと信頼性の観点で導入されたように思う。

ではrunBlockingTestを利用したいのはどういう場合だろうか。

普段ViewModelが呼び出すRepositoryのsuspend関数は実行結果をmockすることが多くあまりピンと来なかったが、KotlinConfのプレゼンテーションではsuspend関数のタイムアウト処理をテストするケースが例として挙げられていた。例えば以下のようなタイムアウトが設定されたsuspend関数があった場合、これをrunBlockingで実行すると5秒待つ必要がある。

```kotlin
suspend fun foo(resultDeferred: Deferred<Foo>) {
    try {
        withTimeout(5_000) {
            resultDeferred.await()
        }
    } catch (e: Exception) {
        println("e: ${e}")
        throw FooException()
    }
}

@ExperimentalCoroutinesApi
@Test(expected = FooException::class)
fun testFooWithTimeout() = runBlocking {
    val uncompleted = CompletableDeferred<Foo>() // this Deferred<Foo> will never complete
    foo(uncompleted)
}
```

一方同じ処理をrunBlockingTestで書くと以下のようになり即座にテストが通る、またDelayControllerによりCoroutineDispatcherのvirtual timeを変更することでtimeOutが絡むテストが容易に記述できる。

```kotlin
// success
@ExperimentalCoroutinesApi
@Test(expected = FooException::class)
fun testFooWithTimeout() = runBlockingTest {
    val uncompleted = CompletableDeferred<Foo>() 
    launch {
        foo(uncompleted)
    }
    advanceTimeBy(5_000)
    uncompleted.complete(Foo("bar"))
}

// failure
@ExperimentalCoroutinesApi
@Test(expected = TitleRefreshError::class)
fun testFooWithTimeout() = runBlockingTest {
    val uncompleted = CompletableDeferred<Foo>() 
    launch {
        foo(uncompleted)
    }
    advanceTimeBy(6_000)
    uncompleted.complete(Foo("bar"))
}
```

## 補足
ここまでrunBlockingTestの用途について書いてみたが現状ExperimentalCoroutinesApiであることに加えて、いくつかクリティカルに思えるissueが上がっていたため導入については要検討という印象である。続報に期待したい。

#### AbstractMethodError when use withTimeout

[https://github.com/Kotlin/kotlinx.coroutines/issues/2307:title]

2021/01/18時点で最新版1.4.2のkotlinx-coroutines-testを利用すると発生するエラー
#### Provided example test for withTimeout fails

[https://github.com/Kotlin/kotlinx.coroutines/issues/1390:title]

ReadMeのExample[5]をそのまま書くと発生するエラー

#### Replace TimeoutCancellationException with TimeoutException
上述の`suspend fun foo` でTimeoutExceptionをそのままthrowせずにtry/catchしてFooExceptionを投げ直している理由。`TimeoutCancellationException is CancellationException, thus is never reported.` とのこと。


[https://github.com/Kotlin/kotlinx.coroutines/issues/1374:title]


#### Use Kotlin Coroutines in your Android App

[https://developer.android.com/codelabs/kotlin-coroutines#9:title]

> runBlockingTest is experimental, and currently has a bug that makes it fail the test if a coroutine switches to a dispatcher that executes a coroutine on another thread. The final stable is not expected to have this bug.


## 参考
[1] [https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/run-blocking-test.html:title]

[2] 

[https://www.youtube.com/watch?v=KMb0Fs8rCRs&t=469s&ab_channel=AndroidDevelopers:embed:cite]

[3]

 [https://www.youtube.com/watch?v=hMFwNLVK8HU&feature=emb_title&ab_channel=JetBrainsTV:embed:cite]

[4] [https://github.com/Kotlin/kotlinx.coroutines/tree/master/kotlinx-coroutines-test#testing-withtimeout-using-runblockingtest:title]