https://kotlinlang.org logo
Title
p

Pablo

03/30/2022, 11:24 PM
One question guys! in my
ViewModel
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] viewModel
In 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?
r

radityagumay

03/30/2022, 11:28 PM
usually use
TestCoroutineDispatcher
p

Pablo

03/30/2022, 11:29 PM
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

andylamax

03/31/2022, 1:01 AM
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

gildor

03/31/2022, 2:03 AM
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

Pablo

03/31/2022, 7:21 AM
sure sorry my view model
@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
@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

gildor

03/31/2022, 7:25 AM
Why do you even need custom dispatcher here? Why not use viewModelScope and just test viewState using turbine?
p

Pablo

03/31/2022, 7:25 AM
CustomDispatcher is to use the IO instead of hardcoding it
Also tried with turbine as follows :
@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

Sam

03/31/2022, 7:42 AM
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

Pablo

03/31/2022, 7:46 AM
The message of the thread are the actual ones
s

Sam

03/31/2022, 7:48 AM
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

Pablo

03/31/2022, 7:50 AM
But even if I do use the testDispatcher.runTest isn't working
j

Joffrey

03/31/2022, 7:55 AM
Why use it as receiver though? And not
runTest(testDispatcher)
?
p

Pablo

03/31/2022, 8:00 AM
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 :
[Test worker @coroutine#1] test
[Test worker @coroutine#2] viewModel
s

Sam

03/31/2022, 8:14 AM
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

Pablo

03/31/2022, 8:27 AM
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

Joffrey

03/31/2022, 8:29 AM
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

Pablo

03/31/2022, 8:32 AM
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

Joffrey

03/31/2022, 9:01 AM
The snippet you showed when using turbine was setting
coEvery
inside the turbine test block, which is too late
p

Pablo

03/31/2022, 9:12 AM
@Joffrey I've edited the new question (last message on #coroutines)
j

Joffrey

03/31/2022, 9:17 AM
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

Pablo

03/31/2022, 9:21 AM
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

Joffrey

03/31/2022, 9:23 AM
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

Pablo

03/31/2022, 9:25 AM
😮
runCurrent
did the magic
Like, I create the
viewModel
and then call
runCurrent
looks like it waits and then do the assertion
j

Joffrey

03/31/2022, 9:27 AM
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

Pablo

03/31/2022, 9:29 AM
So, doing :
runTest(StandardTestDispatcher()) {
 all coEvery stuff
 initialice ViewModel
 runCurrent(because I have the call in the init)
 all coVerify
 assert of mutableStateFlow
}
Looks good?
j

Joffrey

03/31/2022, 9:29 AM
Looks good to me
p

Pablo

03/31/2022, 9:31 AM
Thanks mate 😉
j

Joffrey

03/31/2022, 9:34 AM
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