https://kotlinlang.org logo
Title
а

Андрей Коровин

09/01/2021, 6:29 PM
Hi, I have faced a problem with hot flows while unit testing. I saw people had similar problems but not in the exact way, so it would be nice if someone could explain the following behaviour: 1. This test fails with
java.lang.IllegalStateException: This job has not completed yet
@Test
fun testHotFlow() = coroutinesTestRule.testDispatcher.runBlockingTest {
    val eventChannel = Channel<String>()
    val events = eventChannel.receiveAsFlow()

    val event = events.first()
    assertEquals(event, null)
}
2. This test doesn’t fail
@Test
fun testHotFlow() = coroutinesTestRule.testDispatcher.runBlockingTest {
    val eventChannel = Channel<String>()
    val events = eventChannel.receiveAsFlow()

    launch {
        eventChannel.send("First element")
    }

    val event = events.first()
    assertNotNull(event)
}
e

ephemient

09/01/2021, 6:42 PM
did you mean
.firstOrNull()
?
а

Андрей Коровин

09/01/2021, 7:05 PM
it’s the same with first() or firstOrNull()
e

ephemient

09/01/2021, 7:20 PM
your test fails either way, but
.first()
is wrong even if it worked, because it won't return null
whereas
.firstOrNull()
is only broken because it needs the flow to either emit or terminate (which is also wrong with
.first()
)
а

Андрей Коровин

09/02/2021, 8:32 AM
it needs the flow to either emit or terminate
But why does it fail with “java.lang.IllegalStateException: This job has not completed yet” in the first example. And why it doesn’t fail after it emits something (
eventChannel.send("First element")
)? Isn’t the job still running?
Or is it because of how first/firstOrNull works?
e

ephemient

09/02/2021, 8:45 AM
in the first case, the test dispatcher knows that there are zero runnable children: you cannot possibly proceed, so the test ends instead of deadlocking forever
in the second case, first/firstOrNull terminates as soon as it has an answer, so everything can wrap up
compare to if you used
.shareIn(this)
then the second test wouldn't pass because that launches a coroutine that will keep running
а

Андрей Коровин

09/02/2021, 8:53 AM
zero runnable children
Meaning that nothing can send/terminate the flow?
so everything can wrap up
but isn’t the flow still running? Or it is the
first()
function that creates the job that “has not completed yet”
a coroutine that will keep running
@Test
fun testHotFlow() = coroutinesTestRule.testDispatcher.runBlockingTest  {
    val eventChannel = Channel<String>()
    val events = eventChannel.receiveAsFlow().shareIn(this, SharingStarted.Lazily)

    launch {
        eventChannel.send("First element")
    }

    val event = events.first()
    assertNotNull(event)
}
you are right, it fails, but with more meaningful message:
Test finished with active jobs: ["coroutine#2":StandaloneCoroutine{Active}@244e30dd]
kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["coroutine#2":StandaloneCoroutine{Active}@244e30dd]
e

ephemient

09/02/2021, 8:56 AM
1. nothing is able to proceed 2. the flow is not still running. receiveAsFlow does nothing when there are no collectors, and first stops collecting as soon as it has an answer
if you want to see .first() throw or .firstOrNull() return null, simply close the channel (or use a different kind of flow that ends as well)
а

Андрей Коровин

09/02/2021, 9:05 AM
the channel itself is a private field in my view model, and I collect events flow in my fragment. So I wanted to test that events flow is empty in one case and it’s has a particular value in another
e

ephemient

09/02/2021, 9:07 AM
if the channel never closes, you don't know that is empty
а

Андрей Коровин

09/02/2021, 9:07 AM
I can send the value to the channel by calling a function in my view model. But if I don’t call it, the channel is empty and it results in the exception
you don’t know that is empty
so it’s wrong to assume that it’s empty in the first place