Hello guys! :slightly_smiling_face: I’m in the pro...
# coroutines
j
Hello guys! 🙂 I’m in the process of migrating to the new test api with
runTest
and it’s been an extremely frustrating process so far 😞 I have a very simple view model that doesn’t do a whole lot, something like :
Copy code
fun registerStuff() {
    viewModelScope.launch {
        registerStuffUseCase.execute()
    }
}
And a very simple test (with mockk)
Copy code
@Before
    fun setup() {
        Dispatchers.setMain(StandardTestDispatcher())
    }

    @Test
    fun `SHOULD fetch stuff WHEN called`() =
        runTest {
            // arrange
            val registerStuffUseCase = mockk<RegisterStuffUseCase>(relaxed = true)
            val viewModel = ViewModel(registerStuffUseCase)

            // act
            viewModel.registerStuff()

            // assert
            coVerify(exactly = 1) { registerStuffUseCase.execute() }
        }
And it just doesn’t work. 100% of the time, verify is called before the use case is executed and the test fails. * unless * I add a
advanceUntillIdle()
after calling the VM... Which I can do but that’s like a hundred tests to update and I’m not even sure I’m doing things properly... Anything obvious I’m missing?
b
try
Copy code
Dispatchers.setMain(UnconfinedTestDispatcher())
This should be default replacement for runBlockingTest per migration guide
m
The thing is, that for
StandardTestDispatcher
we always need to call
runCurrent
or advance time, because otherwise its coroutines will never start.
d
Not really familiar with Android, but I don't think the
Main
dispatcher is the culprit. Try
Copy code
fun `SHOULD fetch stuff WHEN called`() =
        runTest(UnconfinedTestDispatcher()) {
This will lead to
launch
and
async
blocks on the top level being entered eagerly. This usually suffices.
Also, yes, this is highlighted in the migration guide. We tried listing all transition issues that we deemed common, but if we missed something, please contact us so that we can potentially update the guide.
b
runTest
captures the TestCoroutineScheduler from Main dispatcher (if it was overridden)
d
Sure, but not the dispatching behavior.
b
ah true, on android it works just because viewModelScope uses the overridden dispatcher
d
Good to know, thanks! In that case, yes, making
Dispatchers.Main
unconfined should also work.
j
I honestly believe it should be explicit in the test that you're waiting for some coroutines to execute before asserting. Relying on unconfined dispatchers seems like a patch to make non-deterministic tests pass. In regular production code, the coroutines launched by
viewModel.registerStuff()
should not be considered guaranteed to be done when the call returns (in general).
scope.launch(...) { ... }
is not guaranteed to be done in general. This might hold true for
viewModelScope
because of the
Dispatchers.Main.immediate
default, but slight deviations would make such assumptions false, and break code. Same goes for tests IMO. And this is what happened to your hundred tests: they relied on immediate/unconfined dispatch instead of explicitly synchronizing coroutines as part of the test. I find it better if the test reads "when calling this, after all coroutines are done, I should see this result"
3
j
runTest(UnconfinedTestDispatcher())
did not work but
Dispatchers.setMain(UnconfinedTestDispatcher())
did
@Joffrey Just to make sure, is your suggestion that
advanceUntilIdle()
is the way to go because it is explicit?
👌 1
j
Yes, or
runCurrent
depending on what you're testing
runTest(UnconfinedTestDispatcher())
did not work because that dispatcher doesn't affect the dispatcher used in the implementation of
viewModel.registerStuff()
(which uses
viewModelScope
, which in turn uses
Dispatchers.Main.immediate
as dispatcher). That's why you had to instead modify what the main dispatcher is.
j
I guess an alternative then would be to have something like val dispatcher = Unconfined, use it in the runTest and inject it in the VM, right?
j
Injecting dispatchers for tests is usually what I go for, but I'm not doing Android dev at all. I'm not sure if it's good practice to use an externally-provided scope or dispatcher instead of `viewModelScope`'s default. Probably setting the main dispatcher as a test dispatcher (like you did) is better 🤷
j
Ok, I got all my tests to run 🙂 Was a painful process though, hopefully, this API is stable and won’t change in a while haha 😅 Anyway, thanks a lot to all those who helped, much appreciated 🙏