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

Cannot test SharedFlow #33

Closed
rakesh-sh2020 opened this issue Mar 24, 2021 · 9 comments
Closed

Cannot test SharedFlow #33

rakesh-sh2020 opened this issue Mar 24, 2021 · 9 comments

Comments

@rakesh-sh2020
Copy link

I have an android viewmdeol class with the following property

private val _trainingNavigationEvents = MutableSharedFlow<NavigationEventTraining>(replay = 0)
    val trainingNavigationEvents = _trainingNavigationEvents.asSharedFlow()

fun navigate(navigationEvent: NavigationEventTraining) {
        viewModelScope.launch {
            _trainingNavigationEvents.emit(navigationEvent)
        }
    }

I am using a SharedFlow as it solves the SingleLiveEvent problem.

The issue arises when I try and unit test the code. I can't see how to use turbine (or supplied primitives) to get it to work.

    @ExperimentalTime
    @Test
    fun `navigate`() = runBlockingTest {
        viewModel.handleIntent(TrainingViewModel.TrainingIntent.ShowQuestions)

        viewModel.navigationEvents.test {
            assertEquals(
                TrainingViewModel.TrainingNavigationEvent.NavigateToQuestions::class,
                expectItem()::class
            )
            cancelAndConsumeRemainingEvents()
        }
    }

and I get

kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1000 ms

I know that a SharedFlow never completes and that may be part of the reason but I have been unable to find any examples of how to do this instead.

I am using Junit 5 and am using a TestCoroutineDispatcher class extension.

@JakeWharton
Copy link
Collaborator

Can you turn this into an executable sample that I can run and debug locally? I don't use MutableSharedFlow, view models, runBlockingTest, or JUnit 5... so yeah it's not entirely clear what's to blame.

@kevincianfarini
Copy link
Contributor

Can you share a full stacktrace? @rakesh-sh2020

I have created a simple reproduction of this bug. My above hypothesis I think is wrong. Here's my test.

  @Test fun foo() = suspendTest {
    val flow1 = MutableSharedFlow<Int>(replay = 0)
    val flow2 = flow1.asSharedFlow()
    flow1.emit(1)

    flow2.test {
      assertEquals(1, expectItem())
      cancelAndConsumeRemainingEvents()
    }
  }

with the corresponding stacktrace:

kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1000 ms
	(Coroutine boundary)
	at app.cash.turbine.ChannelBasedFlowTurbine$expectEvent$2.invokeSuspend(FlowTurbine.kt:238)
	at app.cash.turbine.ChannelBasedFlowTurbine$withTimeout$2.invokeSuspend(FlowTurbine.kt:206)
	at app.cash.turbine.ChannelBasedFlowTurbine.expectItem(FlowTurbine.kt:243)
	at app.cash.turbine.TestFoo$foo$1$1.invokeSuspend(TestFoo.kt:16)

Notice how it's failing at expectItem(FlowTurbine.kt:243)

My suspicion is that your MutableStateFlow replay size of 0 is the culprit. You're emitting an event to a flow with no replay, before you start testing the events of the flow. Your event is getting dropped because there's no subscribers. In my above program, adding a replay = 1 solved the problem. If that's not desirable for your application code, you might consider doing something like the following.

  @Test fun foo() = suspendTest {
    val flow1 = MutableSharedFlow<Int>(replay = 0)
    val flow2 = flow1.asSharedFlow()

    launch {
      flow2.test {
        assertEquals(1, expectItem())
        cancelAndConsumeRemainingEvents()
      }
    }

    flow1.emit(1)
  }

@piotrmadry
Copy link

piotrmadry commented Apr 14, 2021

@kevincianfarini You can try with:

mutableSharedFlow.apply {
    test {
        emit(1)
        assertEquals(1, expectItem())
        emit(2)
        assertEquals(2, expectItem())
    }
}

@dinorahto
Copy link

I know the problem here
For ViewModel you wanna do this:

The ViewModel function will be executed and the flow will be on the right track to be tested with Turbine 😉
Hope this help you

    @ExperimentalTime
    @Test
    fun `navigate`() = suspendTest {
        viewModel.navigationEvents.test {
            viewModel.handleIntent(TrainingViewModel.TrainingIntent.ShowQuestions)
            assertEquals(
                TrainingViewModel.TrainingNavigationEvent.NavigateToQuestions::class,
                expectItem()::class
            )
            cancelAndConsumeRemainingEvents()
        }
    }

@sdoward
Copy link

sdoward commented May 7, 2021

Your problem could be with runBlockingTest as it automatically advances time (as far as I remember)

@Kshitij09-ag
Copy link

@kevincianfarini I can confirm that launching sharedFlow tests in a separate coroutine (Using launch { } inside runBlockingTest { } ) makes it work. Do you know why it works?

@kevincianfarini
Copy link
Contributor

kevincianfarini commented May 21, 2021

@Kshitij09-ag it sets up a flow collector before a value is emitted to it. Once that collector is active and suspended, it will resume when a new emission from the flow is available. From there, the test assertion can be run.

This entire issue happens because StateFlow is HOT. Emitting elements to it when it doesn't have any downstream consumers drops those elements.

The solution @dinorahto provides does the exact same thing my solution does. It starts collecting the flow (with test) before an emission is made (with viewModel.handleIntent).

This issue is not an issue with Turbine, but instead a misunderstanding of the sequence of events that should happen with hot flows. It's possible the documentation should be updated to include this use case.

@kevincianfarini
Copy link
Contributor

@JakeWharton It's likely you can close this issue. If you're up for it, I can write some documentation for how turbine should be used with hot flows.

@JakeWharton
Copy link
Collaborator

Yeah I left #19 open to track adding documentation and I think this falls under that category.

kevincianfarini pushed a commit to kevincianfarini/turbine that referenced this issue May 21, 2021
Hot flows are unique in that they drop emissions when no
consumers are active. We should document this functionality
as it has been surprising to many users so far.

Closes cashapp#19.
References cashapp#33.
kevincianfarini pushed a commit to kevincianfarini/turbine that referenced this issue May 21, 2021
Hot flows are unique in that they drop emissions when no
consumers are active. We should document this functionality
as it has been surprising to many users so far.

Closes cashapp#19.
References cashapp#33.
kevincianfarini pushed a commit to kevincianfarini/turbine that referenced this issue May 21, 2021
Hot flows are unique in that they drop emissions when no
consumers are active. We should document this functionality
as it has been surprising to many users so far.

Closes cashapp#19.
References cashapp#33.
JakeWharton pushed a commit that referenced this issue May 21, 2021
Hot flows are unique in that they drop emissions when no
consumers are active. We should document this functionality
as it has been surprising to many users so far.

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

No branches or pull requests

7 participants