https://kotlinlang.org logo
Title
u

ursus

12/02/2021, 2:26 PM
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
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

stojan

12/02/2021, 2:28 PM
you can use cashapp/trubine to await for items on the state flow
u

ursus

12/02/2021, 2:29 PM
which means what, replacing scope or dispatcher or neither? i.e. wrap turbine
test,
In runBlocking?
m

mkrussel

12/02/2021, 2:38 PM
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

ursus

12/02/2021, 3:06 PM
@mkrussel what do you mean dont expose the flow?
also, what scope to test it in? runBlocking?
m

mkrussel

12/02/2021, 3:09 PM
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

ursus

12/02/2021, 3:11 PM
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

mkrussel

12/02/2021, 3:13 PM
I think turbine will work without you having to change anything
u

ursus

12/02/2021, 3:14 PM
meaning I dont need to replace dispatchers?
m

mkrussel

12/02/2021, 3:15 PM
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

ursus

12/02/2021, 3:16 PM
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

mkrussel

12/02/2021, 3:24 PM
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

ursus

12/02/2021, 3:28 PM
why testdispatcher
m

mkrussel

12/02/2021, 3:31 PM
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

ursus

12/02/2021, 3:36 PM
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

stojan

12/02/2021, 3:50 PM
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

ursus

12/02/2021, 4:08 PM
and your viewmodels are driven by IO in tests, and by Main in actual app?
s

stojan

12/02/2021, 4:13 PM
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

stojan

12/02/2021, 4:14 PM
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

ursus

12/02/2021, 4:19 PM
btw I dont understand why your test finishes,
viewModel.viewState.test {
            awaitItem() shouldBe Loading
            awaitItem() shouldBe problem
        }
should this not wait infinitely, since viewState is noncompleting?
s

stojan

12/02/2021, 4:26 PM
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

ursus

12/02/2021, 4:36 PM
thats odd, I'd expect you need to call
cancelAndIgnoreRemainingEvents
other wise it should suspend there forever, no?
s

stojan

12/02/2021, 4:39 PM
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)