Hi all! I’m having trouble with a unit test. I’m t...
# coroutines
t
Hi all! I’m having trouble with a unit test. I’m trying to validate the behaviour of a StateFlow. But, the StateFlow is only in this particular state until a certain suspend function completes. During testing, my test double uses a delay to simulate a long running task. But, due to the nature of
runBlockingTest()
, this delay is skipped, so this function completes immediately, and the StateFlow essentially bypasses the value I’m trying to test. It’s worth mentioning, I’m using CashApp/Turbine as well.
Copy code
sealed class ViewState {
    object None : ViewState()
    object Loading : ViewState()
    object Some : ViewState()
}

class Foo {
    val viewState = MutableStateFlow<ViewState>(ViewState.None)

    fun updateViewState() {
        viewState.value = ViewState.Loading

        viewState.value = someLongRunningOperation()
    }

    private suspend fun somePotentiallyLongRunningOperation(): ViewState {
        delay(5000)
        return ViewState.Some
    }
}

@Test
fun myTest() = runBlockingTest {
    foo.viewState.test {
        foo.updateViewState()

        assertThat(awaitItem()).isInstanceOf(ViewState.Loading::class.java) // Fails. ViewState is ViewState.Some

        cancelAndConsumeRemainingEvents()
    }
}
e
coroutines 1.6.0?
I also use turbine from squire to test flows more nicely
m
I wonder if this might be helpful https://github.com/Kotlin/kotlinx.coroutines/tree/master/kotlinx-coroutines-test#virtual-time-support-with-other-dispatchers
Calls to 
withContext(<http://Dispatchers.IO|Dispatchers.IO>)
withContext(Dispatchers.Default)
 ,and 
withContext(Dispatchers.Main)
 are common in coroutines-based code bases. Unfortunately, just executing code in a test will not lead to these dispatchers using the virtual time source, so delays will not be skipped in them.
So if I understand correctly if you use
withContext
and don't replace provider for the
TestDispatcher
then delay won't be skipped. There has to be other way - just that I noticed this so wanted to share 🙂
Also, if you don't want to skip delays - do you need to use
runBlockingTest
at all? 🤔 You might just want to create custom
CoroutineScope
as you would in production and run the test with that. (just thinking aloud - not sure if true)
n
Copy code
@Test
fun myTest() = runBlockingTest {
    launch {
        foo.updateViewState()
    }

    runCurrent()
    assertEquals(ViewState.Loading, fot.viewState)
    }
}
You need to run the tested code in a separate coroutine so you can control the clock and verify in the main body.
No a turbine user but I think this should work:
Copy code
@Test
fun myTest() = runBlockingTest {
    launch {
        for.updateViewState()
    }
    runCurrent()
    foo.viewState.test {
    assertThat(awaitItem()).isInstanceOf(ViewState.Loading::class.java) // Fails. ViewState is ViewState.Some

        cancelAndConsumeRemainingEvents()
    }
}
t
Thanks all for the replies. I’ll have a look at these and see what works
None of the above seem to work so far. I’ve been working agains the actual production code, so I’ll try emulating this exact test. I have a feeling Turbine is coming into play here as well
The above is psuedo code, and I hadn’t actually written that exact test. Looks like it actually is working, so perhaps there’s some other problem in my production code. I need to get better at actually writing small repros.
Moving to
runTest()
does seem to solve most of my problems, and revealed a few new ones that make me wonder how the tests ever passed at all!