Hi :android-wave: , I am stuck in choosing the cor...
# coroutines
a
Hi đź‘‹ , I am stuck in choosing the correct test dispatcher -
StandardTestDispatcher
vs
UnconfinedTestDispatcher
. In short,
StandardTestDispatcher
- does not start the coroutines eagerly. - I am stuck on how to start the coroutines when using
StandardTestDispatcher
. For
UnconfinedTestDispatcher
- This starts the coroutines eagerly, but if I have multiple continuous coroutines collected in my ViewModel using
collectLatest
, only the first coroutines is launched and the subsequent coroutines are not launched as the test has only one thread. Can anyone help share how to decide the correct test dispatcher and how to resolved these issues? I have gone through the docs and some resources, but I am still not clear on testing them. thank you color
Note: All coroutines creation and emitting are handled by the ViewModel, but most of the examples I could find show how to test when collection and emission is handled within the tests.
z
I would avoid
UnconfinedTestDispatcher
if at all possible. It's a huge behavior change from how stuff runs in production and will cause headaches down the road, if not right away.
a
Hi @Zach Klippenstein (he/him) [MOD], Thanks for responding. I see this mentioned everywhere to prefer
StandardTestDispatcher
. But I am not able to understand how to launch the coroutines.
Can you please help? A minified example,
Copy code
class MyViewModel(
  coroutineScope: CoroutineScope,
) : ViewModel(
  viewModelScope = coroutineScope,
) {
  val stateFlow1 = MutableStateFlow(1)
  val stateFlow2 = MutableStateFlow(2)

  fun testMethod() {
    viewModelScope.launch {
      getFlow1UseCase().collectLatest {
        stateFlow1.value = 10
      }
    }
    viewModelScope.launch {
      getFlow2UseCase().collectLatest {
        stateFlow2.value = 20
      }
    }
  }
}
Test
Copy code
class MyViewModelTest {
    private lateinit var testDispatcher: TestDispatcher
    private lateinit var testScope: TestScope

    private lateinit var viewModel: MyViewModel

    @Before
    fun setUp() = runTest {
        testDispatcher = StandardTestDispatcher(
            scheduler = testScheduler,
        )
        testScope = TestScope(
            context = testDispatcher,
        )
    }

    @Test
    fun `when flows emits data, then state flows are updated accordingly`() = runTestWithTimeout {
            whenever(
                methodCall = getFlow1UseCase(),
            ).thenReturn(flowOf(10))
            whenever(
                methodCall = getFlow2UseCase(),
            ).thenReturn(flowOf(20))
            initViewModel(
                coroutineScope = testScope,
            )

            viewModel.testMethod()

            assertEquals(10, viewModel.stateFlow1.value)
            assertEquals(20, viewModel.stateFlow2.value)
        }

    private fun initViewModel(
        coroutineScope: CoroutineScope,
    ) {
        viewModel = MyViewModel(
            coroutineScope = coroutineScope,
        )
    }

    private fun runTestWithTimeout(
        block: suspend TestScope.() -> Unit,
    ) = runTest(
        timeout = 3.seconds,
        testBody = block,
    )
}
j
Your test shows you injecting the scope but your actual view model shows you using
viewModelScope
. Which are you actually doing?
If you are actually using
viewModelScope
you'll need to use
Dispatchers.setPain
Dispatchers.setMain
to replace the main dispatcher with the test one. Be sure to set it back in
@After
.
a
Sorry, missed to add that part. I am overriding the
viewModelScope
in my ViewModel constructor.
I am following this to inject the Coroutine scope in the view model and it overrides the
viewModelScope
https://developer.android.com/jetpack/androidx/releases/lifecycle#2.5.0
I am referring this talk by
Márton Braun
.

https://youtu.be/nKCsIHWircA?si=cfDjjS9CzDIjfvuhâ–ľ

Does this mean
async
is always preferred over
launch
even when fire and forget works for my functionality?
z
I don't think this is the cause of whatever issue you're seeing, but this test class is very weird. Why are you calling
runTest
in
setUp
? That doesn't make any sense since the
setUp
method returns before your actual test runs. Then you're also calling
runTest
for your actual test method, but then not using any of the coroutine machinery provided by the test's
runTest
, instead using the stuff from the
runTest
in
setUp
.
And I'm not clear what actual issue you're seeing – you said a coroutine isn't being launched? Why do you think that?
f
I am overriding the
viewModelScope
in my ViewModel constructor.
providing a coroutine scope to the viewmodel constructor does not override the existing
viewmodelScope
, you have to explicitly use the scope that you pass in the constructor to launch your coroutines
a
Hi Zach,
And I'm not clear what actual issue you're seeing – you said a coroutine isn't being launched? Why do you think that?
I have added
println()
like this,
Copy code
fun testMethod() {
    println("getFlow1UseCase before launch")
    viewModelScope.launch {
      println("getFlow1UseCase launch")
      getFlow1UseCase().collectLatest {
        stateFlow1.value = 10
      }
    }
    println("getFlow2UseCase before launch")
    viewModelScope.launch {
    println("getFlow2UseCase launch")
      getFlow2UseCase().collectLatest {
        stateFlow2.value = 20
      }
    }
}
I don't see
getFlow2UseCase launch
being printed, hence I infer that the coroutines is not launched. Regarding the setup, Thanks for pointing it out, I was trying to reuse the
TestDispatcher
initialization code which required
runTest
. I will do some refactoring to see if that is the root cause of my issue.
Hi @Francesc, I am overriding the
viewModelScope
as mentioned here. I am following this to inject the Coroutine scope in the view model and it overrides the
viewModelScope
https://developer.android.com/jetpack/androidx/releases/lifecycle#2.5.0 It is mentioned here that we can provide
CoroutineScope
as a parameter to
ViewModel
to override the
viewModelScope
.
z
Your assertions are being executed immediately after you call launch, so on the standard dispatcher they might not have actually started yet. You’ll want to do something like advance the test scheduler until idle before asserting.