esdrasdl
04/29/2022, 8:50 PMclass SampleViewModel : ViewModel() {
private val interactions = Channel<Any>()
private val state = MutableSharedFlow<Any>(
onBufferOverflow = BufferOverflow.DROP_LATEST,
extraBufferCapacity = 1
)
init {
viewModelScope.launch {
interactions.receiveAsFlow().collect { action ->
processAction(action)
}
}
}
fun flow() = state.asSharedFlow()
fun handleActions(action: Any) {
viewModelScope.launch {
interactions.send(action)
}
}
suspend fun processAction(action: Any) {
state.emit("Show loading state")
// process something...
state.emit("Show success state")
}
}
esdrasdl
04/29/2022, 8:50 PM@Test
fun testHandleActions() = runTest {
val emissions = mutableListOf<Any>()
val job = launch(UnconfinedTestDispatcher(testScheduler)) {
viewModel.flow().collect {
emissions.add(it)
}
}
viewModel.handleActions("")
assertTrue(emissions[0] is String)
assertTrue(emissions[1] is String)
job.cancel()
}
If I test the viewmodel.processAction() it works like a charm. But if I try to test handleActions() I only receive the first emission and then it throws an IndexOutOfBoundsException
If I use the deprecated runBlockingTest, it works
@Test
fun testHandleActionsWithRunBlockingTest() = runBlockingTest {
val emissions = mutableListOf<Any>()
val job = launch {
viewModel.flow().toList(emissions)
}
viewModel.handleActions("")
job.cancel()
assertTrue(emissions[0] is String)
assertTrue(emissions[1] is String)
}
Did I miss something using runTest
block?julian
05/04/2022, 5:51 PMesdrasdl
05/04/2022, 5:52 PMjulian
05/04/2022, 11:22 PMjob.join()
after viewModel.handleActions("")
.
And put println(emissions)
after emissions.add(it)
.
You should see that emissions
is being populated as expected. It is no longer empty.julian
05/04/2022, 11:35 PMIn order to for the spawned-off asynchronous code to actually be executed, one must either yield or suspend the test body some other way, or use commands that control schedulingFrom here. There's an example just below that text that shows using
job.join()
within runTest
.
Even though you use UnconfinedTestDispatcher(testScheduler)
to (I assume) ensure that the launch
coroutine is started before viewModel.handleActions("")
, I think viewModel.flow().collect
suspends indefinitely because all this is happening within a runTest
.julian
05/04/2022, 11:42 PMjob.join()
. But hopefully it gets you closer by addressing why emissions weren't being collected at all.julian
05/04/2022, 11:47 PMviewModel.flow().collect
to viewModel.flow().take(2).collect
will prevent indefinite suspension, and allow the test to pass and complete.julian
05/05/2022, 12:36 AMChannel
is necessary. It seems like you can achieve the same, but simpler, with just a MutableSharedFlow
that you map
or/and onEach
on.esdrasdl
05/05/2022, 12:28 PMviewModel.handleActions("").
. Unfortunately It didn’t work. Regarding using channel. I’m would like to try a MVI approach and I assumed it’s a good idea to have one public function to send eventos to viewmodel. I chose Channel because I guessed it should be simple to use. I guess I’m wrong, =/. BTW I replaced Channel for another MutableSharedFlow and the same problem happened.esdrasdl
05/05/2022, 12:29 PMMutableSharedFlow
and avoid to use Channel ?julian
05/05/2022, 3:12 PMViewModel
. This is so I can run the code without Android dependencies. But this should be okay, since the scope I create is the same as the one that ViewModel
creates i.e. a scope with a SupervisorJob
.julian
05/05/2022, 4:06 PMesdrasdl
05/05/2022, 8:48 PMclass SampleViewModel(
var scope: CoroutineScope? = null
) : ViewModel() {
init {
scope = scope ?: viewModelScope
scope?.launch {
interactions.receiveAsFlow().collect { action ->
processAction(action)
}
}
}
}
julian
05/05/2022, 10:36 PMCoroutineScope
/ CoroutineDispatcher
dependency such that they can be injected. Especially for testing, where it's a major benefit to have everything running on the same dispatcher.
ViewModel
isn't designed to have its dispatcher injected, but you may know that you can set it indirectly in your tests via Dispatchers.setMain(TestCoroutineDispatcher())
. If you're not already doing this in your tests, then timing/coordination will be more tricky, for sure. See this in the official docs:
Note: The viewModelScope property of ViewModel classes is hardcoded to Dispatchers.Main. Replace it in tests by calling Dispatchers.setMain and passing in a test dispatcher.Preferable to isolate functionality as much as possible away from android dependencies, so you can run vanilla non-instrumented tests.
esdrasdl
05/06/2022, 8:50 PM