Skip to content

No longer able to catch exceptions from other coroutines while within runTest after 1.7.0 #3889

Closed
@lucas-livefront

Description

@lucas-livefront

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 runTests.

@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()
        }
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions