为什么在kotlin协同程序中启动吞下异常?

时间:2018-06-05 19:41:43

标签: kotlin kotlin-coroutines

以下测试成功Process finished with exit code 0。注意,此测试会将异常打印到日志中,但不会使测试失败(这是我想要的行为)。

@Test
fun why_does_this_test_pass() {
    val job = launch(Unconfined) {
        throw IllegalStateException("why does this exception not fail the test?")
    }

    // because of `Unconfined` dispatcher, exception is thrown before test function completes
}

正如预期的那样,此测试失败并显示Process finished with exit code 255

@Test
fun as_expected_this_test_fails() {
    throw IllegalStateException("this exception fails the test")
}

为什么这些测试的行为方式不一样?

3 个答案:

答案 0 :(得分:5)

将您的测试与不使用任何协同程序的测试进行比较,但改为启动新线程:

@Test
fun why_does_this_test_pass() {
    val job = thread { // <-- NOTE: Changed here
        throw IllegalStateException("why does this exception not fail the test?")
    }
    // NOTE: No need for runBlocking any more
    job.join() // ensures exception is thrown before test function completes
}

这里发生了什么?就像使用launch的测试一样,如果你运行它,这个测试会传递,但异常会在控制台上打印出来。

因此,使用launch启动新协程与使用thread启动新线程非常相似。如果失败,错误将由thread中的未捕获异常处理程序和CoroutineExceptionHandler launch(请参阅文档中的launch}处理。启动时的例外不是吞下,而是由协程异常处理程序处理

如果您希望异常传播到测试,则应将async替换为join,并将await替换为代码中的GlobalScope.launch。另请参阅此问题:What is the difference between launch/join and async/await in Kotlin coroutines

更新:Kotlin协程最近推出了&#34; Structured Concurrency&#34;避免这种例外的损失。此问题中的代码不再编译。要编译它,你必须明确地说runBlocking { ... }(如在&#34;我确认可以放弃我的例外,这是我的签名&#34;)或将测试包装成{ {1}},在这种情况下,异常不会丢失。

答案 1 :(得分:2)

我能够为测试创建一个抛出CoroutineContext的异常。

val coroutineContext = Unconfined + CoroutineExceptionHandler { _, throwable ->
        throw throwable
    }

虽然这可能不适合生产。也许需要捕获取消例外或其他东西,我不确定

答案 2 :(得分:0)

到目前为止,自定义测试规则似乎是最好的解决方案。

/**
 * Coroutines can throw exceptions that can go unnoticed by the JUnit Test Runner which will pass
 * a test that should have failed. This rule will ensure the test fails, provided that you use the
 * [CoroutineContext] provided by [dispatcher].
 */
class CoroutineExceptionRule : TestWatcher(), TestRule {

    private val exceptions = Collections.synchronizedList(mutableListOf<Throwable>())

    val dispatcher: CoroutineContext
        get() = Unconfined + CoroutineExceptionHandler { _, throwable ->
            // I want to hook into test lifecycle and fail test immediately here
            exceptions.add(throwable)
            // this throw will not always fail the test. this does print the stacktrace at least
            throw throwable 
        }

    override fun starting(description: Description) {
        // exceptions from a previous test execution should not fail this test
        exceptions.clear()
    }

    override fun finished(description: Description) {
        // instead of waiting for test to finish to fail it
        exceptions.forEach { throw AssertionError(it) }
    }
}

我希望通过此post进行改进