https://kotlinlang.org logo
Title
j

Jérémy CROS

01/21/2022, 1:53 PM
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 :
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?
b

bezrukov

01/21/2022, 2:00 PM
try
Dispatchers.setMain(UnconfinedTestDispatcher())
This should be default replacement for runBlockingTest per migration guide
m

marcinmoskala

01/21/2022, 2:04 PM
The thing is, that for
StandardTestDispatcher
we always need to call
runCurrent
or advance time, because otherwise its coroutines will never start.
d

Dmitry Khalanskiy [JB]

01/21/2022, 2:09 PM
Not really familiar with Android, but I don't think the
Main
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.
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

bezrukov

01/21/2022, 2:12 PM
runTest
captures the TestCoroutineScheduler from Main dispatcher (if it was overridden)
d

Dmitry Khalanskiy [JB]

01/21/2022, 2:12 PM
Sure, but not the dispatching behavior.
b

bezrukov

01/21/2022, 2:14 PM
ah true, on android it works just because viewModelScope uses the overridden dispatcher
d

Dmitry Khalanskiy [JB]

01/21/2022, 2:15 PM
Good to know, thanks! In that case, yes, making
Dispatchers.Main
unconfined should also work.
j

Joffrey

01/21/2022, 2:29 PM
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

Jérémy CROS

01/21/2022, 2:46 PM
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?
:yes: 1
j

Joffrey

01/21/2022, 2:49 PM
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

Jérémy CROS

01/21/2022, 2:56 PM
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

Joffrey

01/21/2022, 3:00 PM
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

Jérémy CROS

01/21/2022, 3:53 PM
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 🙏