I'm migrating a test to coroutines 1.6.0 which sho...
# coroutines
l
I'm migrating a test to coroutines 1.6.0 which should check that a suspending function throws an exception. I'm using AssertJ for assertions. This is what the test looked like before:
Copy code
@Test
fun `when no gateway from scanning present then throw`() {
    every { state.gateway } returns null

    assertThatIllegalStateException().isThrownBy { runBlocking { useCase.execute(Params()) } }
}
This is what it looks like after the migration, according to the official migration guide:
Copy code
val exceptions = mutableListOf<Throwable>()
    val customCaptor = CoroutineExceptionHandler { _, throwable ->
        exceptions.add(throwable)
    }

    @Test
    fun `when no gateway from scanning present then throw`() = runTest {
        every { state.gateway } returns null

        launch(customCaptor) {
            useCase.execute(Params())
        }
        advanceUntilIdle()

        assertThat(exceptions).hasSize(1)
            .first().isInstanceOf(IllegalStateException::class.java)
    }
However, the exception is for some reason not caught by the
customCaptor
, but thrown in the test and the test fails.
I works when I simply replace
runBlocking
by
runTest
, but this is not recommended according to the migration guide (it could obscure other problems with the test detected by the
runTest
cleanup procedure).
I don't use any custom dispatcher in the
useCase
under test. Should I inject one and replace it with the
TestScope.coroutineContext
for testing?
The test still throws the exception even when I set a test dispatcher:
Copy code
@BeforeTest
fun setUp() {
  Dispatchers.setMain(StandardTestDispatcher())
}
d
Won't just
Copy code
assertFailsWith<IllegalStateException> { useCase.execute(Params()) }
(without `launch`ing a new coroutine) work? The reason the whole test fails with an exception is that the coroutine you launch fails with that exception. The uncaught exception handler is not used, because, well, the exception is not uncaught, it's propagated using structured concurrency.
l
Yes, this indeed works. Thank you for your quick and helpful answer!
But I still don't understand the explanation in the migration guide.
It says: "... valid uses of TestCoroutineExceptionHandler include: Accessing
uncaughtExceptions
when the uncaught exceptions are actually expected. In this case,
cleanupTestCoroutines
will fail with an exception that is being caught later. It would be better in this case to use a custom
CoroutineExceptionHandler
so that actual problems that could be found by the cleanup procedure are not superseded by the exceptions that are expected. An example is shown below."
d
I answered some of your questions under the issue you opened, but basically, the migration guide is for migration from the old test API, which, in this case, you weren't using at all.
l
Yes, I understand this now.
So the uncaught exceptions in the test would only be exceptions not thrown within a coroutine?
d
"Uncaught exceptions" are the ones that nobody catches. Given that you managed to observe the exception being caught, it's not uncaught. Uncaught exceptions are in general quite rare and can be considered an obscure edge case.
You could create an uncaught exception like this:
Copy code
launch(SupervisorJob()) {
  launch {
    throw IllegalStateException()
  }
}
l
Yes, I guess I misunderstood that exceptions rethrown by coroutines are also uncaught
d
Children of
SupervisorJob
don't have the right to fail the
SupervisorJob
, so they can't propagate the exception upwards, so they have to resort to uncaught exception handlers.
l
Because for me "uncaught" meant kind of "crashing the test/program",
Thank you very much for the explanation!
d
You're welcome! Can the issue be closed then?
l
Yes, I just closed it.