David Corrado
10/17/2024, 7:39 PMDaniel Pitts
10/17/2024, 9:59 PMMarco Pierucci
10/18/2024, 12:53 AMRobert Williams
10/18/2024, 9:20 AMDavid Corrado
10/29/2024, 3:29 PMDaniel Pitts
10/29/2024, 3:33 PMDavid Corrado
10/29/2024, 3:35 PMDavid Corrado
10/29/2024, 3:37 PMDaniel Pitts
10/29/2024, 3:38 PMDavid Corrado
10/29/2024, 3:39 PMDaniel Pitts
10/29/2024, 3:39 PMDaniel Pitts
10/29/2024, 3:39 PMDaniel Pitts
10/29/2024, 3:48 PMimport kotlinx.coroutines.*
import kotlinx.coroutines.test.*
import org.junit.jupiter.api.*
import kotlin.test.*
import kotlin.test.Test
@TestMethodOrder(MethodOrderer.MethodName::class)
class ExampleUnitTest {
@Test
fun `1) runtest success`() = runTest {
assertTrue(true)
}
@Test
fun `2) no runtest success`() {
GlobalScope.launch {
error("error!")
}
}
@Test
fun `3) runTest failure before starting`() = runTest {
assertTrue(
true,
)
}
}
David Corrado
10/29/2024, 3:53 PMDavid Corrado
10/29/2024, 3:58 PMDaniel Pitts
10/29/2024, 4:01 PMkotlinx.coroutines.test.internal.ExceptionCollector
to the kotlinx.coroutines.internal.platformExceptionHandlers
. the ExceptionCollector keeps track of all exceptions that occur once it's been registered. Since the tests aren't run in isolation, the ExceptionCollector receives exceptions.Daniel Pitts
10/29/2024, 4:04 PMDavid Corrado
10/29/2024, 4:05 PMDaniel Pitts
10/29/2024, 4:06 PM@AfterTest
fun forceFailIfException() {
runTest { }
}
This at least makes it fail at the right place 😉David Corrado
10/29/2024, 4:06 PMDavid Corrado
10/29/2024, 4:09 PMDaniel Pitts
10/29/2024, 4:09 PM@Test
fun `2) no runtest success`() = runTest {
CoroutineScope(<http://Dispatchers.IO|Dispatchers.IO>).launch {
delay(1000)
error("error!")
}
}
This throws a wrench into it though.David Corrado
10/29/2024, 4:10 PMDavid Corrado
10/29/2024, 4:10 PMDaniel Pitts
10/29/2024, 4:10 PMdelay
that I'm pointing out.Daniel Pitts
10/29/2024, 4:10 PMDavid Corrado
10/29/2024, 4:10 PMDmitry Khalanskiy [JB]
10/29/2024, 4:13 PMrunTest
). Launching coroutines without a hierarchy of coroutine scopes is an antipattern. It's useful in some cases, but very rarely.
If there is no hierarchy of coroutine scopes, there is no way for`runTest` to normally learn about failures in some unrelated coroutines. It's the same as when one of your threads crashes: the other threads won't learn about it.
However, people do write code without structured concurrency support. To help them find the places where their code crashes, we've added special behavior: runTest
keeps track of all unhandled exceptions in the system, and if there are any, reports them somewhere, even if they are unrelated to the test. Note that if you launch coroutines outside of structured concurrency, there is no way for runTest
to even learn that a coroutine is there: GlobalScope.launch
is completely unrelated to runTest
, and it can't know to wait for the coroutine to finish.
The way to fix this is not to let your code crash your applications.
> This at least makes it fail at the right place 😉
Not always. It's subject to race conditions: if you write something like GlobalScope.launch { delay(2.seconds); throw IllegalStateException }
, then (any) runTest
will only catch the error two seconds later.Daniel Pitts
10/29/2024, 4:14 PMDaniel Pitts
10/29/2024, 4:14 PMDavid Corrado
10/29/2024, 4:15 PMDmitry Khalanskiy [JB]
10/29/2024, 4:16 PMDavid Corrado
10/29/2024, 4:20 PMDmitry Khalanskiy [JB]
10/29/2024, 4:22 PMviewModelScope
, making it impossible for runTest
to know anything about coroutines running there.David Corrado
10/29/2024, 4:24 PMDmitry Khalanskiy [JB]
10/29/2024, 4:25 PMDavid Corrado
10/29/2024, 4:27 PMViewModel
implementation that uses viewModelScope
to launch a coroutine that loads data:"David Corrado
10/29/2024, 4:29 PMDmitry Khalanskiy [JB]
10/29/2024, 4:30 PMviewModelScope
is the way to go, but then I don't see any technical possibility for runTest
to report errors happening in viewModelScope
accurately. If you have any ideas about how to solve this problem on our side, please share them: we are also not fans of errors that are reported far away from their sources.Robert Williams
10/29/2024, 4:31 PMDavid Corrado
10/29/2024, 4:32 PMDaniel Pitts
10/29/2024, 4:34 PMDmitry Khalanskiy [JB]
10/29/2024, 4:35 PMviewModelScope
also crash the whole application, so if the problem is limited to viewModelScope
, it shouldn't be widespread: those coroutines must be written in such a way that they never crash anyway.Daniel Pitts
10/29/2024, 4:35 PMRobert Williams
10/29/2024, 4:36 PMDavid Corrado
10/29/2024, 4:36 PMDavid Corrado
10/29/2024, 4:37 PMDavid Corrado
10/29/2024, 4:38 PMDavid Corrado
10/29/2024, 4:39 PMRobert Williams
10/29/2024, 4:41 PMDaniel Pitts
10/29/2024, 4:41 PMDaniel Pitts
10/29/2024, 4:43 PMfun theTest() {
var value = false
viewModelScope.launch {
value = true
error("Bah")
}
assertTrue(value)
}
David Corrado
10/29/2024, 4:43 PMDavid Corrado
10/29/2024, 4:44 PMDavid Corrado
10/29/2024, 4:45 PMRobert Williams
10/29/2024, 5:02 PMRobert Williams
10/29/2024, 5:04 PMInstantTaskExecutorRule
change anything in how you expect these tests to behave? I know it's also something you have to remember to use but I'm just surprised because I don't think I've ever had a ViewModel test that didn't require this to passDavid Corrado
10/29/2024, 5:11 PMOliver.O
10/29/2024, 6:06 PMViewModel
setups for testing, but I'm wondering if using the constructor ViewModel(viewModelScope = this)
within a runTest
invocation's TestScope
would make it play with structured concurrency.