One question guys! in my `ViewModel` I have injec...
# coroutines
p
One question guys! in my
ViewModel
I have injected a
CoroutineDispatcher
something like
Copy code
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
Copy code
println("[${Thread.currentThread().name}] viewModel")
println("[${Thread.currentThread().name}] test")
And in the console I see :
[Test worker] test
[Test worker @coroutine#3] viewModel
In the test I'm doing it as :
Copy code
private val testDispatcher = UnconfinedTestDispatcher()
private val testCoroutineScope = TestScope(testDispatcher)

@Test
fun test() = testCoroutineScop.runTest {
 val viewModel = FooViewModel(testDispatcher)
 ...
}
What I'm missing?
r
usually use
TestCoroutineDispatcher
p
It's deprecated, that's why I'm using the recomended one
I've tried it also but did not work, still in different workers...
Any idea @radityagumay?
Looks like when doing the
viewModelScope.launch{ withContext(ioDispatcher){...}}
is jumping to another coroutine and it doesn't work my test.
a
Why don't you use the
StandardTestDispatcher
? 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 statements
g
Could you probably share some self-contained example, it’s hard to understand what is going on in your VM and in your test code
p
sure sorry my view model
Copy code
@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())
        }
    }
And this is the test
Copy code
@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)
        }
@andylamax @gildor addded ViewModel + tests
g
Why do you even need custom dispatcher here? Why not use viewModelScope and just test viewState using turbine?
p
CustomDispatcher is to use the IO instead of hardcoding it
Also tried with turbine as follows :
Copy code
@Test
    fun test() =
        runTest {
            val viewModel = LocationsViewModel(locationsUseCase, testDispatcher)
            viewModel.viewState.test {
               
                coEvery { locationsUseCase() } returns buildLocations
                assertEquals(LocationsViewModel.Foo(buildLocations), awaitItem())
            }
        }
But still not getting the
coEvery
because viewModel the coroutine is coroutine#4 and the rest are on the workers
Soemthing I'm doing wrong here...
s
The test code you just shared is actually different from the example in your first message. Specifically, in your first message you created a
TestScope
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.
p
The message of the thread are the actual ones
s
In that case it looks like the call to
runTest
is not actually using the
testDispatcher
that you created higher up (unless there's some setup code you didn't show)
p
But even if I do use the testDispatcher.runTest isn't working
j
Why use it as receiver though? And not
runTest(testDispatcher)
?
p
You mean
runTest(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 :
Copy code
[Test worker @coroutine#1] test
[Test worker @coroutine#2] viewModel
s
That seems to resolve your question around threads. It sounds like the remaining issues might be with the mocks you're using, not with the coroutines themselves.
every
and
coEvery
shouldn't be sensitive to which coroutine/thread they're running on, but when they're called makes a big difference
Make sure you're calling
coEvery
before the code under test tries to access the mock
1
p
Thanks @Sam but I already did this, the coEvery is before the creation of the ViewModel, the thing is once I run the test I see the first print and once the test fails then print the other (the viewModel one) I've re-updated the question here https://kotlinlang.slack.com/archives/C1CFAFJSK/p1648715050171599
j
How exactly do you expect the viewModel's coroutine to already have run when you assert the value of the state flow? You have no synchronization code that would force it to happen, and there is no suspension point that would even allow that coroutine to run. You reach the assert, which fails, and then the other coroutine runs
p
That's why @gildor said to use Turbine I'd say, but already tried it and didn't work, but let me try it again.
j
The snippet you showed when using turbine was setting
coEvery
inside the turbine test block, which is too late
p
@Joffrey I've edited the new question (last message on #coroutines)
j
I have never used Turbine, so I'm not sure if that's the case, but I would assume it returns the first value that comes from the flow, and in that case you still have the initial value + the new value to await. Did you try awaiting for a second item? Also, I don't know if turbine's
test
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)
p
I can remove turbine if you try to add a snipped of how to do it without Turbine (how to wait until the element)
j
If you use
runCurrent
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 initially
p
😮
runCurrent
did the magic
Like, I create the
viewModel
and then call
runCurrent
looks like it waits and then do the assertion
j
Exactly. This is what makes the test dispatcher useful, it gives you control over all running coroutines so you can wait for them to run like this, or control fake time.
runCurrent
runs all coroutines until they reach a
delay
.
advanceUntilIdle
runs all coroutines until there is no more to run.
p
So, doing :
Copy code
runTest(StandardTestDispatcher()) {
 all coEvery stuff
 initialice ViewModel
 runCurrent(because I have the call in the init)
 all coVerify
 assert of mutableStateFlow
}
Looks good?
j
Looks good to me
p
Thanks mate 😉
j
You're welcome! When you have a
launch
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
🙌🏽 1