benkuly
11/08/2023, 8:16 AMadvanceUntilIdle
does not "wait" when there is dispatcher switch within a coroutine? For example
delay(100) // skipped
withContext(otherDispatcher){
delay(100)
}
delay(200) // not called at all on advanceUntilIdle
At least that's what I observed with ktor MockEngine.
If this is really the case, it should be mentioned somewhere, because it has cost us hours of debugging.ross_a
11/08/2023, 9:20 AMJoffrey
11/08/2023, 9:28 AMbenkuly
11/08/2023, 9:29 AMDmitry Khalanskiy [JB]
11/08/2023, 9:46 AMDo I understand it right, thatYes. It is mentioned in every place I can think of.does not "wait" when there is dispatcher switch within a coroutine?advanceUntilIdle
advanceUntilIdle
, runCurrent
, etc., all only work with the tasks that the test dispatchers know about. If some other coroutine dispatcher is responsible for the task, there's no way for the test module to know about it and ensure it's completed.
delay(100) // skipped
val waitForResult = launch(otherDispatcher){
delay(100)
}
delay(200) // not called at all on advanceUntilIdle
I don't know what you mean by this. If this whole block of code executes in a test dispatcher and is followed by advanceUntilIdle
, then both the first delay(100)
and delay(200)
will be skipped. delay(100)
in the launch
won't, as it doesn't use virtual time.
In this simple case, you don't even need advanceUntilIdle
, you can just waitForResult.join()
instead, and it will work out of the box. In general, most usages of advanceUntilIdle
that I've seen in the wild are not needed and can be replaced with plain and simple synchronization between coroutines, the way you'd write it in production code.benkuly
11/08/2023, 10:20 AMclass Test {
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun test() = runTest {
launch {
println(1)
delay(100) // skipped
println(2)
withContext(Dispatchers.Default) {
delay(100)
}
println(3)
delay(200) // not called at all on advanceUntilIdle
println(4)
}
advanceUntilIdle()
println("-------------")
}
}
It prints the following:
1
2
-------------
3
4
I expect:
1
2
3
4
-------------
Dmitry Khalanskiy [JB]
11/08/2023, 10:23 AMjoin
after println(4)
, you'll get what you expect.launch
and advanceUntilIdle
, the test will only become more idiomatic.benkuly
11/08/2023, 10:28 AMlaunch
is hided in view model. The launch(Dispatchers.Default)
is actually a ktor post request, I changed it above to withContext
. This is just a simplified example to show the problem.ross_a
11/08/2023, 10:29 AMbenkuly
11/08/2023, 10:33 AMross_a
11/08/2023, 10:37 AMclass Test {
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun test() = runTest {
println(1)
delay(100) // skipped
println(2)
withContext(Dispatchers.Default) {
delay(100)
}.join()
println(3)
delay(200) // not called at all on advanceUntilIdle
println(4)
println("-------------")
}
}
Dmitry Khalanskiy [JB]
11/08/2023, 10:38 AMadvanceUntilIdle
and others are unintuitive when other dispatchers come into play. This is only this way for historical reasons; if we designed the framework today from scratch, they wouldn't be included.
That's one of the reasons why we are looking into providing suitable replacements for them: https://github.com/Kotlin/kotlinx.coroutines/issues/3919benkuly
11/08/2023, 10:50 AMI feel that opinion goes against the grain a bit I'm afraid
I am not sure why you wouldn't just rewrite it asAs mentioned above, the
launch
is hidden in a not suspending view model method. This is just an example to show the actual problem.Dmitry Khalanskiy [JB]
11/08/2023, 10:55 AMbenkuly
11/08/2023, 11:17 AMadvanceUntilIdle
is just useless.