-
Notifications
You must be signed in to change notification settings - Fork 1.9k
No longer able to catch exceptions from other coroutines while within runTest
after 1.7.0
#3889
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Could you give an example of a useful test that uses this? Why would one ever want to intentionally throw exceptions in a way that they are passed to the uncaught exception handler? |
The current use case for this is within our If more information is required, I can certainly get it for you. |
|
Your inference is correct. The app would also crash in production. Is your hypothesis that there is no case where an exception would be thrown from a coroutine outside of the hierarchy of the test scope? |
So, do I get it right that you want to ensure that your app crashes if the conditions are right? That's a very unusual requirement. Typically, people try to avoid writing code that crashes the app and test that it does not crash under any circumstances. |
@dkhalanskyjb The idea here is to "fail fast" during development so that we catch any cases where required input data are missing and fix them before they hit production. We want to test that our "fail fast" logic is working. This is useful for us to both know and test. This used to be possible to do and with recent changes it has become much more cumbersome and awkward. If the recommendation here is to wrap the call in |
@dkhalanskyjb If it helps to have a standard reference, here is an example from the Now In Android Google sample project : internal class TopicArgs(val topicId: String) {
constructor(savedStateHandle: SavedStateHandle) :
this(URLDecoder.decode(checkNotNull(savedStateHandle[topicIdArg]), URL_CHARACTER_ENCODING))
} That @HiltViewModel
class TopicViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val userDataRepository: UserDataRepository,
topicsRepository: TopicsRepository,
userNewsResourceRepository: UserNewsResourceRepository,
) : ViewModel() {
private val topicArgs: TopicArgs = TopicArgs(savedStateHandle) // <---- Crash happens here if the "topicId" data is missing.
val topicId = topicArgs.topicId That is the same as what's going on in our case (an exception would be thrown if its missing); the only difference here is that they would get the exception when constructing the VM and we will get in inside a coroutine just based on how our VMs get constructed. This kind of "developer exception" use case is not unusual, but now we are looking for guidance on how to test it given these recent changes to |
I see. Yes, in your case, nesting |
Sounds good, thanks @dkhalanskyjb! We just wanted to make sure those nested |
Actually, @dkhalanskyjb this solution doesn't work as we expected. 🤦 To be clear here I will use slightly different tests to illustrate the issues here. As a reminder, here is the method we want to assert throws. This is a simple mock up, but in practice we would test some core app functionality that is wrapped up in a coroutine scope that it creates itself. // An example function to test
private fun somethingThatBuildsACoroutineScopeAndThrows() {
CoroutineScope(Dispatchers.Unconfined).launch {
throw IllegalStateException("Something is happening")
}
} Using
|
You can work around that with runTest {
TestScope().runTest {
println("this doesn't fail")
}
} |
@dkhalanskyjb In this case we see the same error as the "unscoped @Test
fun `building new TestScope runTest test`() {
val block: () -> Unit = {
TestScope().runTest { somethingThatBuildsACoroutineScopeAndThrows() }
}
runTest {
val throwable = assertThrows(IllegalStateException::class.java) {
block()
}
// Added print statements to prove that assertions are succeeding
println("Throwable type is correct: ${throwable is IllegalStateException}")
println("Throwable message is correct: ${throwable.message == "Something is happening"}")
assertEquals(
throwable.message, "Something is happening"
)
}
}
|
Ok, I see the problem: both Will this work for you? val block: () -> Unit = {
TestScope().runTest { CoroutineScope(Dispatchers.Unconfined).launch { throw IllegalStateException("Something is happening") } }
}
runBlocking {
try {
block()
} catch (throwable: Exception) {
println("Caught exception: $throwable")
// Added print statements to prove that assertions are succeeding
println("Throwable type is correct: ${throwable is IllegalStateException}")
println("Throwable message is correct: ${throwable.message == "Something is happening"}")
}
} |
@dkhalanskyjb So is it officially recommended that I not use runTest to run suspending code whenever I also need to catch uncaught exceptions? If it is, I can see there being lingering issues here. This seems like something that would easily be done incorrectly and is difficult to debug. |
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: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 nestrunTest
s.The text was updated successfully, but these errors were encountered: