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())bezrukov
01/21/2022, 2:02 PMmarcinmoskala
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.Dmitry Khalanskiy [JB]
01/21/2022, 2:11 PMbezrukov
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()) didJérémy CROS
01/21/2022, 2:48 PMadvanceUntilIdle() is the way to go because it is explicit?Joffrey
01/21/2022, 2:49 PMrunCurrent depending on what you're testingJoffrey
01/21/2022, 2:54 PMrunTest(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