【问题标题】:Why does launch swallow exceptions in kotlin coroutines?为什么在 kotlin 协程中启动会吞下异常?
【发布时间】:2018-11-15 09:28:31
【问题描述】:

以下测试通过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")
}

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

【问题讨论】:

    标签: kotlin kotlin-coroutines


    【解决方案1】:

    将您的测试与以下不使用任何协程但启动新线程的测试进行比较:

    @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。另请参阅此问题:What is the difference between launch/join and async/await in Kotlin coroutines

    更新:Kotlin 协程最近引入了“结构化并发”的概念来避免这种异常丢失。此问题中的代码不再编译。要编译它,您必须明确说出GlobalScope.launch(如“我确认可以放开我的异常,这是我的签名”)或将测试包装到runBlocking { ... },在这种情况下异常不是丢失。

    【讨论】:

    • 我删除了 runBlocking { job.join() },因为它引起了混乱。这个测试只是一个简单的例子,但我真正的实现使用launch,因为我不需要.await()对结果。我想确保异常使我的应用程序崩溃。对于测试,我想确保如果在测试方法完成之前发生异常,则测试失败
    • 就像线程一样,如果你想让异常崩溃你的应用程序,那么你应该使用自定义异常处理程序(协程的协程异常处理程序,线程的未捕获异常处理程序)。
    • 有道理。我为此想出了一个 JUnit TestRule - 但是有更好的解决方案吗? stackoverflow.com/a/52301537/891242
    【解决方案2】:

    我能够创建一个异常抛出 CoroutineContext 进行测试。

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

    虽然这可能不适合生产。可能需要捕捉取消异常什么的,我不确定

    【讨论】:

    • 您能找到更稳健的方法吗?这种行为非常令人沮丧,也是我对 RxJava 最怀念的事情。
    • 到目前为止,我的想法是解决这个问题的最佳方法是生成一个捕获协程上下文的错误的测试规则,然后在 tearDown 中检查协程是否捕获任何错误并失败相应的测试我觉得应该有更好的方法
    【解决方案3】:

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

    /**
     * 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 改进它

    更新:只需使用 runBlocking - 就像 Roman 建议的那样。

    【讨论】:

      猜你喜欢
      • 2018-05-26
      • 1970-01-01
      • 1970-01-01
      • 2020-11-15
      • 2016-06-08
      • 1970-01-01
      • 2010-11-02
      • 2011-01-05
      • 1970-01-01
      相关资源
      最近更新 更多