kevin.cianfarini
02/25/2023, 3:40 PMadvanceTimeBy
but since delays are skipped, using it doesn't make a difference.
Is testing that currentTime
has been incremented as you expect the proper approach?Dmitry Khalanskiy [JB]
02/25/2023, 5:15 PMTimeSource
that tracks the virtual time, with all the usual methods available on it, as in https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/common/test/TestCoroutineSchedulerTest.kt#L317-L319kevin.cianfarini
02/25/2023, 5:39 PMadvanceTimeBy
if delays are always skipped?Dmitry Khalanskiy [JB]
02/25/2023, 5:49 PMdelay(50.milliseconds)
or something and call advanceTimeBy(51.milliseconds)
in the test body.kevin.cianfarini
02/25/2023, 6:12 PMDmitry Khalanskiy [JB]
02/25/2023, 6:20 PMrunTest {
launch {
delay(50)
println("2")
delay(50)
println("4")
}
println("1")
advanceTimeBy(51)
println("3")
}
Dmitry Khalanskiy [JB]
02/25/2023, 6:22 PMdelay(n)
is ignored completely; it just means that it doesn't actually perform the delay. The relative order of tasks that use virtual time does obey the delays.Robert Williams
02/27/2023, 10:23 AMadvanceTimeBy
was designed exactly for this, never had problems and is the recommended approach in the READMEkevin.cianfarini
02/27/2023, 3:01 PMsuspend fun foo() {
delay(10_000)
}
@Test fun testFoo() = runTest {
foo()
println(currentTime) // Prints 10_000
}
In the above test, the function testFoo
will automatically skip the delay in foo
. If you inspect TestScheduler.currentTime
after the invocation to foo
it will be 10_000
.
Now, how does advanceTimeBy
play into this? What if I did the following?
@Test fun testFooDifferently() = runTest {
foo()
advanceTimeBy(1L)
assertFooIsStillSuspended() // Pretend this exists
advanceTimeBy(9999L)
assertFooIsFinished() // Pretend this exists, too.
}
In the above function, since I have the ability to manually control virtual time, I would expect foo
to still be suspended when the call to assertFooIsStillSuspended
is called. Because delay skipping is unconditionally applied everywhere the delay immediately gets skipped and foo
is not in fact still suspended when that assertion is made. It has already completed.
This is a scenario I’m currently running into which makes me question the validity of unconditionally applying delay skipping in the test scheduler.Dmitry Khalanskiy [JB]
02/27/2023, 3:03 PM@Test fun testFooDifferently() = runTest {
delay(10_000)
advanceTimeBy(1L)
assertFooIsStillSuspended() // Can't even exist
advanceTimeBy(9999L)
assertFooIsFinished() // Also can't exist
}
suspend
functions are not different from normal ones in the regard that advanceTimeBy(1L)
can only run after delay(10_000)
has finished.kevin.cianfarini
02/27/2023, 3:04 PMadvanceTimeBy
happens before a delay call is queued in the dispatcher?kevin.cianfarini
02/27/2023, 3:05 PMkevin.cianfarini
02/27/2023, 3:06 PMlaunch
the delay and assert that the job is still active.Dmitry Khalanskiy [JB]
02/27/2023, 3:10 PMSo does delay skipping get “paused”I don't understand at all what it means to "pause" delay skipping. Make delays take real time? No, this doesn't happen.
if a call toLike this?happens before a delay calladvanceTimeBy
runTest {
advanceTimeBy(1000)
delay(1000)
}
then, first, advanceTimeBy
spins pending tasks for one second of virtual time (making the current virtual time 1000), and then, delay
waits for another second of virtual time (making the current virtual time 2000).
The real test I’m working with here is with a flow and I’m asserting whether or not elements get emitted.I don't see the relation to
advanceTimeBy
and not sure whether the advice makes sense, but you could try the Turbine library in general for testing flows.
I could also amend the above test toThen it would be completely different.the delay and assert that the job is still active.launch
launch
puts the work into a queue, it does not immediately run its body before returning. I provided an example of advanceTimeBy
with launch
above, and there is one in the README as well.kevin.cianfarini
02/27/2023, 3:25 PMfun testFoo() = runTest {
launch {
println(1) // executes during runCurrent()
delay(1_000) // suspends until time is advanced by at least 1_000
“suspends until time is advanced by at least 1_000
.” Why aren’t these delays skipped and currentTime
incremented for us? Under what conditions does delay skipping magic happen for us, and under what conditions do we need to do it manually via advanceTimeBy
?Dmitry Khalanskiy [JB]
02/27/2023, 3:29 PMWhy aren’t these delays skipped andWe want to support interleaving between tasks.incremented for us?currentTime
runTest {
launch {
delay(50)
println("2")
delay(100)
println("4")
}
launch {
delay(10)
println("1")
delay(100)
println("3")
}
}
So, delay
calls must actually suspend the coroutine, instead of being stepped over.
Under what conditions does delay skipping magic happenThere's a queue of tasks, and when the only tasks left are the delayed ones, the delays needed to get to those tasks get skipped.
Dmitry Khalanskiy [JB]
02/27/2023, 3:30 PMunder what conditions do we need to do it manually viaUnder very rare ones. I'd say that, when you actually need?advanceTimeBy
advanceTimeBy
, you'll know it. As I said in one of the first messages, it's only occasionally useful.kevin.cianfarini
02/27/2023, 3:32 PMwhen the only tasks left are the delayed ones, the delays needed to get to those tasks get skipped.Is this called out in the documentation anywhere? If so I must have missed it. It’s really unintuitive and I know I’m not the only one operating under the assumption that virtual time is completely manual via
advanceTimeBy
(which is not the case).Dmitry Khalanskiy [JB]
02/27/2023, 3:34 PMDmitry Khalanskiy [JB]
02/27/2023, 3:36 PMrunBlockingTest
)? Then, time control was much more prominent: using TestCoroutineScope
, one had to call advanceTimeBy
etc manualy.kevin.cianfarini
02/27/2023, 3:36 PMBill Phillips
02/27/2023, 4:34 PMI’d say that, when you actually needThat’s the question I have: as a tester, I think, “The code I am testing has some time-dependent functionality. That means I need to control the clock”. And every time I have ever had code with time-dependent functionality, that code has been time-dependent due to calls to, you’ll know it.advanceTimeBy
delay
So: that is confusing. Because the answer here seems to say, “If your code uses delay
to implement time-based functionality, then the standard test scheduler’s virtual time and advanceTimeBy
will not solve your problem”Bill Phillips
02/27/2023, 4:36 PMadvanceTimeBy
needed? What is reliant on virtual time, other than delay
?Bill Phillips
02/27/2023, 4:43 PMdelay
for time-dependent functionality, what is the appropriate scheduler setup in my unit test? How do I test, e.g “X did not happen because my timer has not expired yet”?Robert Williams
02/27/2023, 4:56 PMDmitry Khalanskiy [JB]
02/27/2023, 5:01 PMBecause the answer here seems to say, “If your code usesIt does not say that. It says that usually, automatic delay skipping is enough and fine-grained time control is only rarely needed.to implement time-based functionality, then the standard test scheduler’s virtual time anddelay
will not solve your problem”advanceTimeBy
when isSee an example I provided at the top of this thread.needed?advanceTimeBy
What is reliant on virtual time, other than?delay
withTimeout
.
what is the appropriate scheduler setup in my unit test?What do you mean? We don't provide much configurability, there's barely anything to set up, everything should work out of the box.
How do I test, e.g “X did not happen because my timer has not expired yet”?Without an idea what your code looks like, tough to say but in general, it could look something like
runTest {
val time = 10.seconds
startTimer(time)
delay(9.seconds)
assertTimerNotFired()
delay(1.seconds)
assertTimerFired()
}
Robert Williams
02/27/2023, 5:05 PMdelay
there rather than advanceTimeBy
? I guess they’re equivalent because of the delay skipping but advanceTimeBy seems more descriptive for a test 🤔Dmitry Khalanskiy [JB]
02/27/2023, 5:10 PMdelay
because this way, tests look much simpler. You don't have to know anything about time control: in fact, if you replace runTest
with runBlocking
, you get some code that would work the same way, only the delays will take 10 seconds of real time and be non-deterministic. In short, the principle of least power.Dmitry Khalanskiy [JB]
02/27/2023, 5:11 PMadvanceTimeBy
and such when a simple delay
that works everywhere also works well here?Robert Williams
02/27/2023, 5:15 PMRobert Williams
02/27/2023, 5:15 PMDmitry Khalanskiy [JB]
02/27/2023, 5:17 PMadvanceTimeBy
is in the "controlling virtual time" section. So, you have to explicitly want this power to search for this.Dmitry Khalanskiy [JB]
02/27/2023, 5:23 PMadvanceTimeBy
. For example, if you injected TestScope
somewhere and want to spin some coroutines before the test starts, there's really no other choice. Also, advanceTimeBy
is guaranteed to stop processing tasks once it reaches the target time but before processing any task scheduled for exactly the target time, which is important if you want to test an exact execution order ("first, task A runs for 50 milleseconds, then, task B runs for 2 milleseconds", etc). This is difficult to ensure with `delay`: if several `delay`s are scheduled for the same moment, it's up to the test scheduler to choose which one of them runs first.
As I said above, if actually you need it, you know you need it.kevin.cianfarini
02/27/2023, 5:25 PMAs I said above, if actually you need it, you know you need it.It’s my opinion that more clear examples of how virtual time interacts with (or doesn’t interact with) delay, withTimeOut, timers, etc. It’s unclear when “you know you need it”. I thought I needed it but turns out I didn’t.
kevin.cianfarini
02/27/2023, 5:27 PMIf several coroutines are waiting to be executed next, the one scheduled after the smallest delay will be chosen. The virtual time will automatically advance to the point of its resumption.Doesn’t as clearly explain the virtual time behavior as you explained it earlier.
There’s a queue of tasks, and when the only tasks left are the delayed ones, the delays needed to get to those tasks get skipped.
Dmitry Khalanskiy [JB]
02/27/2023, 5:32 PMmore clear examples of how virtual time interacts with (or doesn’t interact with) delay, withTimeOut, timers, etc.I honestly don't know how to do it better than what we did in the README: there are examples with both
delay
and withTimeout
that demonstrate all there is to it. If you can suggest new good examples or improvements for the older ones, please do, we accept pull requests for the documentation.Bill Phillips
02/27/2023, 5:40 PMrunTest {
var counter = 0
backgroundScope.launch {
counter++
delay(50.milliseconds)
counter++
delay(50.milliseconds)
counter++
}
runCurrent()
assertEquals(1,counter)
advanceTimeBy(51.milliseconds.inWholeMilliseconds)
assertEquals(2, counter)
advanceTimeBy(50.milliseconds.inWholeMilliseconds)
assertEquals(3, counter)
}
Bill Phillips
02/27/2023, 5:41 PMBill Phillips
02/27/2023, 5:57 PMBill Phillips
02/27/2023, 5:59 PMclass MyFakeService {
val serviceRequests = Turbine<Request>()
val serviceResponses = Turbine<Response>()
suspend fun callService() {
serviceRequests += Request()
return serviceResponses.awaitItem()
}
}
Bill Phillips
02/27/2023, 6:01 PMrunTest {
val service = MyFakeService()
val subject = MySubject(service)
backgroundScope.launch { subject.interactWithService() }
service.serviceRequests.awaitItem()
assertThat(subject.someObservableState).isEqualTo(EXPECTED_VALUE_BEFORE_RESPONSE)
service.serviceResponses += Response()
// do more testing
}
Bill Phillips
02/27/2023, 6:02 PM