Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Coroutine launch not waiting with StandardTestDispatcher on ViewModel #3880

Closed
Tgo1014 opened this issue Sep 6, 2023 · 11 comments
Closed

Coroutine launch not waiting with StandardTestDispatcher on ViewModel #3880

Tgo1014 opened this issue Sep 6, 2023 · 11 comments
Labels

Comments

@Tgo1014
Copy link

Tgo1014 commented Sep 6, 2023

Describe the bug

If a ViewModel has a viewModelScope.launch{} on the init{}, even after setting Dispatchers.setMain(StandardTestDispatcher()) the coroutine runs automatically.

Provide a Reproducer

class TestViewModel : ViewModel() {
    val state = MutableStateFlow("A")
    init {
        viewModelScope.launch {
            state.update { "B" }
        }
    }
}
class TestViewModelTest {
    private lateinit var viewModel: TestViewModel
    @Before
    fun setup() {
        Dispatchers.setMain(StandardTestDispatcher())
        viewModel = TestViewModel()
    }
    @Test
    fun test() = runTest {
        assert(viewModel.state.value == "A") // Fails, value is "B"
        advanceUntilIdle()
        assert(viewModel.state.value == "B")
    }
}
@Tgo1014 Tgo1014 added the bug label Sep 6, 2023
@dkhalanskyjb dkhalanskyjb added test and removed bug labels Sep 6, 2023
@dkhalanskyjb
Copy link
Collaborator

Why is this a problem?

@Tgo1014
Copy link
Author

Tgo1014 commented Sep 6, 2023

Why is this a problem?

When the main is set as StandardTestDispatcher shouldn't the launch wait until advanceUntilIdle()?

I've found this issue when migrating from a old version of coroutines where this exactly scenario worked but now breaks a lot of tests that we have.

@dkhalanskyjb
Copy link
Collaborator

When the main is set as StandardTestDispatcher shouldn't the launch wait until advanceUntilIdle()?

It shouldn't. Tasks that are launch-ed with a StandardTestDispatcher get put into a queue, and then these tasks get executed by runTest in order. In your example, the task launched in init gets registered earlier than the test body does, and so executes first.

In fact, advanceUntilIdle is very rarely useful.

I've found this issue when migrating from a old version of coroutines where this exactly scenario worked but now breaks a lot of tests that we have.

I don't understand the purpose of the test you're showing. If a TestViewModel is created from the main thread in production (and it most likely is: does anyone create view models from background threads?), the viewModelScope.launch will be entered immediately, there's no chance to observe "A" in practice. Why are you testing something that won't be observed in production?

@Tgo1014
Copy link
Author

Tgo1014 commented Sep 6, 2023

these tasks get executed by runTest in order

So runTest automatically launches everything that was queued? This is a important info piece that I missed. With this in mind I changed the test to this and it works:

   @Test
    fun test() {
        assert(viewModel.state.value == "A")
        runTest {
            advanceUntilIdle()
            assert(viewModel.state.value == "B")
        }
    }

Weirdly if I don't use advanceUntilIdle() the update{} runs after the second assert.

I don't understand the purpose of the test you're showing.

The tests we have are setting a mock value for a interactor that the init{} calls. With the old version it was possible to create the VM in the @Before and then every test would set the expected response from the mock and then call advanceUntilIdle() to check if the value was set properly.

I tried to research this issue and couldn't find anyone having this exact problem I had, i also asked in the slack kotlin before opening an issue here but looks like my lack of knowledge about runTest got me.

I'll close this now, thanks for the clarification @dkhalanskyjb!

@Tgo1014 Tgo1014 closed this as completed Sep 6, 2023
@greggiacovelli
Copy link

greggiacovelli commented Jan 22, 2024

@dkhalanskyjb The runTest executing any queued launches prior, is this a new change introduced as of 1.7.0?. I was seeing this behavior change as well and I could not find where it was documented otherwise.

@dkhalanskyjb
Copy link
Collaborator

The runTest executing any queued launches prior, is this a new change introduced as of 1.7.0?

Nope, it was like that from the start. If you see behavioral changes, please find a case when some code behaves differently on different versions of kotlinx-coroutines and report it as a bug.

@Tgo1014
Copy link
Author

Tgo1014 commented Jan 23, 2024

Nope, it was like that from the start.

I'm not sure it was, I opened this issue here exactly because I was updating the coroutines to a newer version and some tests that worked previously stopped working after the upgrade.

@dkhalanskyjb
Copy link
Collaborator

Ok, thanks, will look into it.

@greggiacovelli
Copy link

I see, I have resolved my issue. The original assumption is that the behavior added as a result of addressing #1205 was already in the library. However with it not, many tests propogated asserting correct functionality where it was not. Thank you! This is awesome

@dkhalanskyjb
Copy link
Collaborator

@greggiacovelli, #1205 is already in the library. It's a completely separate issue.

@greggiacovelli
Copy link

No I apologize between 1.6.4 and 1.7.3 there were a lot of additions. The most noticable was run test now failing on any coroutine exception. So the codebase I I was working within was experiencing swallowed nullpointerexceptions. As of 1.7.0 many changes seem to have gone in relating to addressing issue 1205. So the tests under 1.6.4 were incorrectly passing. The tests in my code base should have failed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants