Jérémy CROS
01/21/2022, 1:53 PMrunTest
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 :
fun registerStuff() {
viewModelScope.launch {
registerStuffUseCase.execute()
}
}
And a very simple test (with mockk)
@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?bezrukov
01/21/2022, 2:00 PMDispatchers.setMain(UnconfinedTestDispatcher())
marcinmoskala
01/21/2022, 2:04 PMStandardTestDispatcher
we always need to call runCurrent
or advance time, because otherwise its coroutines will never start.Dmitry Khalanskiy [JB]
01/21/2022, 2:09 PMMain
dispatcher is the culprit. Try
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.bezrukov
01/21/2022, 2:12 PMrunTest
captures the TestCoroutineScheduler from Main dispatcher (if it was overridden)Dmitry Khalanskiy [JB]
01/21/2022, 2:12 PMbezrukov
01/21/2022, 2:14 PMDmitry Khalanskiy [JB]
01/21/2022, 2:15 PMDispatchers.Main
unconfined should also work.Joffrey
01/21/2022, 2:29 PMviewModel.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"Jérémy CROS
01/21/2022, 2:46 PMrunTest(UnconfinedTestDispatcher())
did not work but
Dispatchers.setMain(UnconfinedTestDispatcher())
didadvanceUntilIdle()
is the way to go because it is explicit?Joffrey
01/21/2022, 2:49 PMrunCurrent
depending on what you're testingrunTest(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érémy CROS
01/21/2022, 2:56 PMJoffrey
01/21/2022, 3:00 PMJérémy CROS
01/21/2022, 3:53 PM