Im confused about the testing apis Am I expected ...
# coroutines
u
Im confused about the testing apis Am I expected to always inject the coroutine scope to stuff I want to test, even if I dont want to control time progress? Or is the correct way to make stuff synchronous/blocking with injecting Undispatched dispatcher driving my private scope? Asking for a friend who wants to test something like this
Copy code
class Syncer {
   private val scope = CoroutineScope(SupervisorJob() + <http://Dispatcher.Io|Dispatcher.Io>)
   private val _state = MutableStateFlow<State>(Idle)
   val state: Flow<State> get() = _state
   
   fun sync() {
      scope.launch {
         _state.value = Syncing
         someSuspendingApiAndDbStuff()
         _state.value = Synced
      }
   }
}
I want to test if I see correct
state
emissions if I call
sync()
s
you can use cashapp/trubine to await for items on the state flow
u
which means what, replacing scope or dispatcher or neither? i.e. wrap turbine
test,
In runBlocking?
m
Turbine won't work since they don't expose the flow. Instead you probably need to be able to inject a different dispatcher into
Syncer
for testing. Then you could inject unconfined to make the launch happen immediately.
u
@mkrussel what do you mean dont expose the flow?
also, what scope to test it in? runBlocking?
m
I was wrong. I thought the
state
variable was returning the current value of the flow, but it is returning the actual flow, so you can use turbine to test it. Should probably change the return type to be
Flow
instead of
MutableFlow
or else there is no point in having the two properties.
u
yea thats just a pseudo code I typed on a phone; I fixed it for ya
anyways my question is, why replace the dispatcher, if im running in runBlocking (or, which scope should I run in when using turbine)
m
I think turbine will work without you having to change anything
u
meaning I dont need to replace dispatchers?
m
yes. Using runBlocking for the tests and turbine should work, but I haven't used turbine enough to be positive. Try it out and see what happends
u
honestly I'm kind of surprised there is not a 1st party tool for this, this is like 50% of coroutine usecases in apps; its either thing that exposes a plain suspend function, or a stateholder which owns its scope and runs stuff on it
follow up questions 1. do I need to cancel the Syncer.scope? does it matter in tests if the gradle process will get killed anyways? 2. if Syncer was a ViewModel, i.e. driven by main thread dispatcher; what do I replace it with, Dispatchers.Default?
m
I would probably replace it with a
TestDispatcher
I'm not sure about the need for cancelling scopes. I don't know if it can be garbage collected if there are no references to
Syncer
anymore and there are no jobs running.
u
why testdispatcher
m
Just easier to manager. Using Default would actually add multithreading to the test. Using the dispatcher from the
runBlocking
scope might make more sense though. It would then be the same thread as the test, which is likely more similar to how the view model works with MainScope.
u
okay that makes kind of sense, If however use some singlethreaded dispatcher instead, I dont trust the testdispatcher schenenigans with time advancing and skipping delays etc
s
by using turbine you DO NOT have to inject the dispatchers. The code will run on multiple threads but Turbine will wait until a certain number of items are emitted in the flow (depending on the test). Example usage: https://github.com/LordRaydenMK/SuperheroesAndroid/blob/534225d9c60b49122706d06b75[…]erheroesapp/superheroes/superheroslist/SuperheroesListKtTest.kt
u
and your viewmodels are driven by IO in tests, and by Main in actual app?
s
I typically use
Default
for whatever is done in VM, network layer uses
IO
, I switch to
Main
in the
Fragment
btw your testing framework makes test cases suspend functions right? any idea as what scope does it use? I see you dont need to set scope explicitly
s
that looks like I made a mistake 🙈 (using
IO
) but doesn't matter that much
yup, using kotest I can use suspend functions in tests: https://kotest.io/
u
btw I dont understand why your test finishes,
Copy code
viewModel.viewState.test {
            awaitItem() shouldBe Loading
            awaitItem() shouldBe problem
        }
should this not wait infinitely, since viewState is noncompleting?
s
exactly the reason I'm using turbine.... the flow doesn't complete, but my test passes/finishes. I'm not sure how it's implemented internally but I assume it's not a plain
collect
(which would suspend infinitely)
u
thats odd, I'd expect you need to call
cancelAndIgnoreRemainingEvents
other wise it should suspend there forever, no?
s
I actually wrote the code with earlier Turbine version, where I don't think that was a requirement.... and I only did updates with dependabot..... so I'm not sure exactly what's happening there (but tests are passing)