Pablo
03/30/2022, 11:24 PMViewModel
I have injected a CoroutineDispatcher
something like
class FooViewModel @Inject constructor(private val dispatcher: CoroutineDispatcher){}
Then when I'm trying to do the test I'm seeing that the test and the code in the ViewModel
are working on a different thread. I know it because I've added some logs
println("[${Thread.currentThread().name}] viewModel")
println("[${Thread.currentThread().name}] test")
And in the console I see :
[Test worker] test
[Test worker @coroutine#3] viewModelIn the test I'm doing it as :
private val testDispatcher = UnconfinedTestDispatcher()
private val testCoroutineScope = TestScope(testDispatcher)
@Test
fun test() = testCoroutineScop.runTest {
val viewModel = FooViewModel(testDispatcher)
...
}
What I'm missing?radityagumay
03/30/2022, 11:28 PMTestCoroutineDispatcher
Pablo
03/30/2022, 11:29 PMviewModelScope.launch{ withContext(ioDispatcher){...}}
is jumping to another coroutine and it doesn't work my test.andylamax
03/31/2022, 1:01 AMStandardTestDispatcher
?
Also, I don't think if your code is running on separate threads, just separate coroutines in the same thread.
This may likely be because you code is launching separate coroutines.. Can you share the code block that contains your print statementsgildor
03/31/2022, 2:03 AMPablo
03/31/2022, 7:21 AM@HiltViewModel
class LocationsViewModel @Inject constructor(
private val locationsUseCase: LocationsUseCase,
@IO private val dispatcher: CoroutineDispatcher,
) : ViewModel() {
init {
getLocations()
}
private val _viewState = MutableStateFlow<Foo>(Foo.Empty)
val viewState: StateFlow<Foo> = _viewState
private fun getLocations() {
viewModelScope.launch(dispatcher) {
println("[${Thread.currentThread().name}] viewModel")
_viewState.value = Foo.Data(locationsUseCase())
}
}
@ExperimentalCoroutinesApi
class LocationsViewModel {
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
private val testDispatcher = StandardTestDispatcher()
private val locationsUseCase = mockk<LocationsUseCase>()
@Test
fun test() =
runTest {
println("[${Thread.currentThread().name}] runTest")
coEvery { locationsUseCase() } returns buildLocations()
val viewModel = LocationsViewModel(locationsUseCase, testDispatcher)
assertEquals(MainViewModel.Foo.Data(buildLocations), viewModel.viewState.value)
}
gildor
03/31/2022, 7:25 AMPablo
03/31/2022, 7:25 AM@Test
fun test() =
runTest {
val viewModel = LocationsViewModel(locationsUseCase, testDispatcher)
viewModel.viewState.test {
coEvery { locationsUseCase() } returns buildLocations
assertEquals(LocationsViewModel.Foo(buildLocations), awaitItem())
}
}
coEvery
because viewModel the coroutine is coroutine#4 and the rest are on the workersSam
03/31/2022, 7:42 AMTestScope
which you use to run the test, but in your later code examples you didn't include that. Is that intentional? It might account for some of the problems.Pablo
03/31/2022, 7:46 AMSam
03/31/2022, 7:48 AMrunTest
is not actually using the testDispatcher
that you created higher up (unless there's some setup code you didn't show)Pablo
03/31/2022, 7:50 AMJoffrey
03/31/2022, 7:55 AMrunTest(testDispatcher)
?Pablo
03/31/2022, 8:00 AMrunTest(testDispatcher){...}
? still saying that the actual and expected are not the same (the actual is empty looks like the coverify is not doing the work, but I still see this :
[Test worker @coroutine#1] test
[Test worker @coroutine#2] viewModel
Sam
03/31/2022, 8:14 AMevery
and coEvery
shouldn't be sensitive to which coroutine/thread they're running on, but when they're called makes a big differencecoEvery
before the code under test tries to access the mockPablo
03/31/2022, 8:27 AMJoffrey
03/31/2022, 8:29 AMPablo
03/31/2022, 8:32 AMJoffrey
03/31/2022, 9:01 AMcoEvery
inside the turbine test block, which is too latePablo
03/31/2022, 9:12 AMJoffrey
03/31/2022, 9:17 AMtest
is a suspension point, but you may still have a race between both coroutines.
You could use runCurrent
before your assert to ensure concurrent coroutines are run before that point (without the need for Turbine actually)Pablo
03/31/2022, 9:21 AMJoffrey
03/31/2022, 9:23 AMrunCurrent
or advanceUntilIdle
you don't really need to wait for the flow's element anymore, you should be able to access the state flow's value directly like you did initiallyPablo
03/31/2022, 9:25 AMrunCurrent
did the magicviewModel
and then call runCurrent
looks like it waits and then do the assertionJoffrey
03/31/2022, 9:27 AMrunCurrent
runs all coroutines until they reach a delay
. advanceUntilIdle
runs all coroutines until there is no more to run.Pablo
03/31/2022, 9:29 AMrunTest(StandardTestDispatcher()) {
all coEvery stuff
initialice ViewModel
runCurrent(because I have the call in the init)
all coVerify
assert of mutableStateFlow
}
Joffrey
03/31/2022, 9:29 AMPablo
03/31/2022, 9:31 AMJoffrey
03/31/2022, 9:34 AMlaunch
somewhere you can think of it as enqueuing the piece of code to the dispatcher's internal task queue, and the caller continues to run the code after the launch
(that's how asynchronous stuff works). Of course it depends on the dispatcher (undispatched magic actually makes the launch block run immediately for instance, same for Dispatchers.Main.immediate
which is the default in some Android built-in scopes, and multithreaded dispatchers can run the code in parallel), but in the standard test dispatcher (and regular single-threaded dispatchers) that's how things usually work.
That's why you need to think about when you expect each coroutine to run during your tests. I usually don't like leaving it to chance, you have ways to control that and avoid races