Closed
Description
Describe the bug
Pre-1.7.0
we were doing the correct thing by using the Thread ’s uncaught exception handler for exceptions thrown in coroutines that are encapsulated in a tested function like so:
// An example function to test
private fun somethingThatBuildsACoroutineScopeAndThrows() {
CoroutineScope(Dispatchers.Unconfined).launch {
throw IllegalStateException("Something is happening")
}
}
// The utility for testing it correctly before 1.7.0.
fun <T : Throwable> assertThrowsInCoroutine(
expectedThrowable: Class<T>,
block: () -> Unit,
): T {
val originalHandler = Thread.getDefaultUncaughtExceptionHandler()
var throwable: Throwable? = null
Thread.setDefaultUncaughtExceptionHandler { _, t -> throwable = t }
try {
block()
} catch (t: Throwable) {
// Manually catch this here just in case the exception is not in a coroutine
throwable = t
}
Thread.setDefaultUncaughtExceptionHandler(originalHandler)
return assertThrows(expectedThrowable) {
throwable?.let { throw it }
}
}
// An example test
@Test
fun `happy test`() = runTest {
// Do some other suspend function work... thus requiring the `runTest` wrapper.
assertThrowsInCoroutine(IllegalStateException::class.java) {
somethingThatBuildsACoroutineScopeAndThrows()
}
}
But as of 1.7.0
if we try this, the exception goes uncaught and the test bombs. In this PR it seems this behavior was explicitly removed so that users who aren't handling failures in other coroutines experience test failure. I would like to know how we can still correctly catch these exceptions.
Additional Note
We can work around this work around by just wrapping our test function in runTest
. This is not ideal, because as I understand it, we shouldn't nest runTest
s.
@Test
fun `wrap in runTest for success`() = runTest {
// Do some other suspend function work... thus requiring the `runTest` wrapper.
assertThrowsInCoroutine(IllegalStateException::class.java) {
runTest {
somethingThatBuildsACoroutineScopeAndThrows()
}
}
}