以下测试成功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")
}
为什么这些测试的行为方式不一样?
答案 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进行改进