I'm trying to understand what coroutines-test virt...
# coroutines
m
I'm trying to understand what coroutines-test virtual time is all about ( https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-test/ ). I tried using withContext(Dispatchers.Default.limitedParallelism(1)) - but when something happens inside the main context, it seems like that doesn't take effect. I honestly find this virtual time thing really unhelpful when it comes to testing real code. It breaks a lot normal coroutine use cases out of the box. Issue is discussed here on Github, but it seems like no change/plan for change as of yet: https://github.com/Kotlin/kotlinx.coroutines/issues/3179
j
Could you please give more details about your use case in the issue? It would be nice to gather the information around this topic in one place, so people having similar problems can potentially find a solution or contribute use cases. Also it usually yields productive discussions with @Dmitry Khalanskiy [JB].
m
Thanks - I added some use case info on the issue itself. There are a lot of times when a timeout is needed in the real code e.g. network calls, database operations, etc. I also use timeouts in my real code when I am using coroutines to wrap callbacks e.g. using callbackFlow and CompleteableDeferred. The virtual clock will also result in completabledeferred.await(timeout) failing.
Also worth mentioning: it breaks turbine tests which are commonly used for testing flows / viewmodels etc ( https://github.com/cashapp/turbine ) - turbine tests have a timeout.
Hope that makes the affected use cases clearer
j
Thanks, yes I think some of those were already discussed, but it's ok and good to reiterate. I like the recap.
m
OK will watch and see what happens... I'm just a bit confused because my understanding was that runTest was a general purpose multiplatform wrapper to test anything that involves coroutines (given that on JVM it needs to use runblocking and on Javascript the test needs to return a promise). Is it more intended for testing coroutine internals?
k
runTest is supposed to be the environment under which you test coroutines code. Many tests which use runTest could otherwise use something like runBlocking, but that makes it impossible to test on JS for example. It is not intended for testing coroutines internals, it’s intended for general use. Generally I agree that virtual time should be more explicit than it is now. I’ve always wanted something like this:
Copy code
@Test fun foo() = runTest {
  // some code 
  
  withVirtualTime { // this: VirtualTimeManager
    this.advanceTimeBy(...)
  }  

  // more code
}
👍 1
This could potentially even separate out delay skipping and manual time control, which myself and others have found confusing.
Copy code
@Test fun foo() = runTest {
  // some code 
  
  withVirtualTime { // this: VirtualTimeManager
    this.advanceTimeBy(...)
  }  

  withDelaySkipping {
    // code that skips delays and advances time for you automatically. 
  }

  // more code
}
👍 1
👌 1
j
@Mike Dawson your understanding is correct. Initially
runBlockingTest
was JVM only, so the difference with
runBlocking
mostly lied on test-specific sugar like virtual time. However, when it became multiplatform, it became the default (only?) way to test coroutines on other platforms (even when you don't need/want virtual time), but the default behaviour wasn't changed. I also believe we shouldn't have virtual time by default, but that's a debate for the issue itself.
m
It's the only way I know of to run multiplatform coroutine tests other than a roll-your-own expect/actual approach (which doesn't seem like an ideal state of affairs given that coroutines, multiplatform, and JS are stable/soon to be stable). Is there anything else that runTest does on JS? I remember having problems with delay skipping on JS tests before.
k
runTest also has a default timeout
also things like
backgroundScope
which are convenient
m
Thanks, hopefully runTest moves towards a more sensible default, in the meantime, I will probably try and put in my own expect/actual wrapper
• Or, even if not changing any default (though that would seem sensible), just provide a simple option to disable virtual time
j
I think providing a way to disable is what they intend to do. With something like a coroutine context element that you could pass.
👍 1
But you should be able to do that already, by switching to other dispatchers. It would be nice to add a reproducer in the issue for a case where this approach doesn't work for you (you mentioned something running on the main thread)
m
I still haven't 100% figured out what made it unhappy, will try and see if I can put a reproducer up tomorrow. I was able to use the withContext(Dispatchers.Default.limitedParallelism(1)) as per the output on one of my projects and it worked, but when I am testing viewmodels that use the main dispatcher, that doesn't seem to work. What I was using was running in jvmTest, but should move to commonTest. For now I have it using runBlocking. Will see what I can do to put a demo
j
You don't have to to use
Dispatchers.Default
to fix the problem, though. Even
Dispatchers.Main
should work too I believe (I you do need Main), so long as you don't use the test dispatcher anymore
k
Perhaps relevant question but how do I disable delay skipping while still using virtual time? Ie I want to always advance time manually, is that possible today?
j
What do you mean exactly? I don't see what you would gain compared to the current state. With virtual time, adding a
delay()
just moves the virtual clock. It's not really that it is skipped per se, but rather that the time it took is virtual. If you have 2 concurrent pieces of code, one with and one without delay, the one without will run first. Using
advanceTimeBy
and similar is just a way to move the virtual time in places without delay. Do you have an example in mind of something you cannot do right now and which would be enabled by your suggestion?
d
Hi! Sorry I'm late to the party.
There are a lot of times when a timeout is needed in the real code e.g. network calls, database operations, etc.
I answered your point under the issue, but will repeat it here: yes, that's true, sometimes tests do need to do real network calls and database operations (though usually these are replaced with mocks).
I tried using withContext(Dispatchers.Default.limitedParallelism(1)) - but when something happens inside the main context, it seems like that doesn't take effect.
Yes, in
Dispatchers.Main
, time will get skipped if you call
Dispatchers.setMain
with a test dispatcher. This is never a problem, because you must never-ever run blocking code in
Dispatchers.Main
. Not network calls, not database access, not even computations so long you have to wrap them in a
withTimeout
. The main thread is the one drawing the UI, and if you run such long operations on the main thread, the UI will freeze. On Android, it will manifest in ANRs: https://developer.android.com/topic/performance/vitals/anr
I'm just a bit confused because my understanding was that runTest was a general purpose multiplatform wrapper to test anything that involves coroutines (given that on JVM it needs to use runblocking and on Javascript the test needs to return a promise).
Yep, that's more or less the case. I wouldn't call it "general purpose" as it's heavily opinionated by design. In some aspects, it's for the reasons of historical evolution, but generally, it's because it's a test framework, not a test library that would provide you with a bunch of functions for constructing your own test environment.
runTest
is supposed to "just work" for the vast majority of cases.
Is it more intended for testing coroutine internals?
Nope, it's not suitable for that. It's too inflexible for foundational code. The idea is that the people actually writing foundational coroutines code understand the internals well enough to be able to write their own testing infrastructure.
``` withVirtualTime { // this: VirtualTimeManager
this.advanceTimeBy(...)
} ```
The more I'm working on the test framework, the harder it is for me to understand the need to have
advanceTimeBy
or
advanceUntilIdle
. I have not yet seen a class of real-life tests where using these instead of just relying on delay-skipping results in cleaner code. If someone here is a heavy user of
advanceTimeBy
, please send me these tests, and we'll see if your tests can't actually be rewritten more clearly.
delay skipping and manual time control, which myself and others have found confusing
Yeah, manual time control is tricky. I'd advise to avoid it.
I also believe we shouldn't have virtual time by default, but that's a debate for the issue itself.
Yes. If you have some questions you want clarified, feel free to ask them here so that we don't create extra noise under the issue itself, but if you have a point regarding it that you think wasn't raised before, please post it there.
Even
Dispatchers.Main
should work too I believe (I you do need Main), so long as you don't use the test dispatcher anymore
If there are any reasons not to use test dispatchers for
Dispatchers.Main
, please also share them.