LoginSignup
2
2

More than 3 years have passed since last update.

kotlinx-coroutines-testのrunBlockingTestについて

Last updated at Posted at 2021-01-17

前回に引き続き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同様に現在のスレッドをブロックしてコルーチンを起動することが可能。

@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に変更した場合

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秒待つ必要がある。

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が絡むテストが容易に記述できる。

// 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]

2
2
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
2
2