I have a piece of code in an Android Kotlin project similar to below, where I use the flow builder method to generate an infinite loop of periodic emissions:
fun doSomething(): Flow<Int> = flow {
var i = 0
while (true) {
emit(i++)
delay(5000L)
}
}
I am then trying to unit test this flow using the (very useful!) Turbine library, as below:
#Test
fun myTest() = runTest {
doSomething().test {
assertEquals(expected = 0, actual = awaitItem())
assertEquals(expected = 1, actual = awaitItem())
assertEquals(expected = 2, actual = awaitItem())
cancelAndIgnoreRemainingEvents()
}
}
My understanding of the runTest method from the coroutines-test library is that it automatically skips any calls to delay within that scope, but the above test is failing with a timeout exception. I've tried littering log statements throughout, and only the first assertEquals call is being triggered. The latter two are never reached because the call to delay from within the flow is apparently blocking the test.
Is this expected behaviour when working with a Flow builder? If so, is there any way of controlling the passage of time in my scenario?
It looks like this is a bug in Turbine 0.8.0 and 0.9.0-SNAPSHOT. It doesn't inherit the test configuration, which can skip the delays. See the issue here: https://github.com/cashapp/turbine/issues/84
If you want to test now, then you can do so manually.
import kotlin.test.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.runTest
#OptIn(ExperimentalCoroutinesApi::class)
class DelayTest {
fun doSomething(): Flow<Int> = flow {
var i = 0
while (true) {
println("${System.currentTimeMillis()} emitting $i")
emit(i++)
delay(5000L)
}
}
#Test
fun testFlow() = runTest {
val result = doSomething().take(3).toList()
assertEquals(listOf(0, 1, 2), result)
}
}
Or, as a workaround, you can make the flow use the test dispatcher using flowOn(). This changes the context upstream and does not "leak downstream".
#Test
fun testFlowOnWorkaround() = runTest {
val testFlow = doSomething()
.flowOn(UnconfinedTestDispatcher(testScheduler))
testFlow.test {
assertEquals(expected = 0, actual = awaitItem())
assertEquals(expected = 1, actual = awaitItem())
assertEquals(expected = 2, actual = awaitItem())
cancelAndIgnoreRemainingEvents()
}
}
As another workaround you can first collect the flow normally to a list. Thanks to runTest{} the delays are skipped. Then you can transform it back into a flow, and test that flow.
#Test
fun testFlowToListWorkaround() = runTest {
val myFlow = doSomething().take(3).toList().asFlow()
myFlow.test {
assertEquals(expected = 0, actual = awaitItem())
assertEquals(expected = 1, actual = awaitItem())
assertEquals(expected = 2, actual = awaitItem())
cancelAndIgnoreRemainingEvents()
}
}
Versions:
Kotlin 1.6.21
Coroutines 1.6.1
Kotlin Test 1.6.21
Related
I'm doing some exercises to learn Flows in Kotlin, and I found some issues which I cannot understand.
When using a MutableStateFlow, in the next example it only prints the number 3. I would expect to print 0 to 3 instead. One could say that maybe is going too fast, or I should put a delay, but this seems to me a patch if such is the case, since if it is true that if sending MutableStateFlow data too fast makes it skip some values, then is something to consider every single time when using it.
val flow = MutableStateFlow<Int>(0)
fun main(): Unit = runBlocking {
launch {
flow.collect {
println(it)
}
}
(0..3).forEach {
flow.emit(it)
}
}
// Expected to print 0, 1, 2, 3
// Printing only 3
Next, I tried to use a MutableSharedFlow instead, but it emits nothing at all, not even 3. Same code as above but replacing the flow with:
val flow = MutableSharedFlow<Int>()
MutableStateFlow cannot be used here because its behavior does not allow to get every value
so I used SharedFlow
Example with SharedFlow:
val flow = MutableSharedFlow<Int>()
fun main(): Unit = runBlocking {
val scope = // scope
flow
.onEach {
println(it)
}
.launchIn(scope)
(0..3).forEach {
flow.emit(it)
}
// delay to wait for println
launch {
delay(10000)
}
}
viewModelScope blocks UI in Jetpack Compose
I know viewModelScope.launch(Dispatchers.IO) {} can avoid this problem, but how to use viewModelScope.launch(Dispatchers.IO) {}?
This is my UI level code
#Composable
fun CountryContent(viewModel: CountryViewModel) {
SingleRun {
viewModel.getCountryList()
}
val pagingItems = viewModel.countryGroupList.collectAsLazyPagingItems()
// ...
}
Here is my ViewModel, Pager is my pagination
#HiltViewModel
class CountryViewModel #Inject constructor() : BaseViewModel() {
var countryGroupList = flowOf<PagingData<CountryGroup>>()
private val config = PagingConfig(pageSize = 26, prefetchDistance = 1, initialLoadSize = 26)
fun getCountryList() {
countryGroupList = Pager(config) {
CountrySource(api)
}.flow.cachedIn(viewModelScope)
}
}
This is the small package
#Composable
fun SingleRun(onClick: () -> Unit) {
val execute = rememberSaveable { mutableStateOf(true) }
if (execute.value) {
onClick()
execute.value = false
}
}
I don't use Compose much yet, so I could be wrong, but this stood out to me.
I don't think your thread is being blocked. I think you subscribed to an empty flow before replacing it, so there is no data to show.
You shouldn't use a var property for your flow, because the empty original flow could be collected before the new one replaces it. Also, it defeats the purpose of using cachedIn because the flow could be replaced multiple times.
You should eliminate the getCountryList() function and just directly assign the flow. Since it is a cachedIn flow, it doesn't do work until it is first collected anyway. See the documentation:
It won't execute any unnecessary code unless it is being collected.
So your view model should look like:
#HiltViewModel
class CountryViewModel #Inject constructor() : BaseViewModel() {
private val config = PagingConfig(pageSize = 26, prefetchDistance = 1, initialLoadSize = 26)
val countryGroupList = Pager(config) {
CountrySource(api)
}.flow.cachedIn(viewModelScope)
}
}
...and you can remove the SingleRun block from your Composable.
You are not doing anything that would require you to specify dispatchers. The default of Dispatchers.Main is fine here because you are not calling any blocking functions directly anywhere in your code.
I have a listener that may never be called. But, if it is called at least once, I'm reasonably sure that it will be called many more times. I'm a fan of Flows, so I've wrapped it in a callbackFlow() builder. To guard against waiting forever, I want to add a time out. I'm trying to build flow operator that will throw TimeOut of some kind if the first element of the flow takes too long to be emitted. Here is what I have.
fun <T> Flow<T>.flowBeforeTimeout(ms: Long): Flow<T> = flow {
withTimeout(ms){
emit(first())
}
emitAll(this#flowBeforeTimeout.drop(1))
}
And it works a little, these JUnit4 tests pass. There are more passing tests, but I'm omitting them for brevity.
#Test(expected = CancellationException::class)
fun `Throws on timeout`(): Unit = runBlocking {
val testFlow = flow {
delay(200)
emit(1)
}
testFlow.flowBeforeTimeout(100).toList()
}
#Test
fun `No distortion`(): Unit = runBlocking {
val testList = listOf(1,2,3)
val resultList = testList
.asFlow()
.flowBeforeTimeout(100)
.toList()
assertThat(testList.size, `is`(resultList.size))
}
However, this test is not passing.
// Fails with: Expected: is <1> but: was <2>
#Test
fun `Starts only once`(): Unit = runBlocking {
var flowStartCount = 0
val testFlow = flow {
flowStartCount++
emit(1)
emit(2)
emit(3)
}
testFlow.flowBeforeTimeout(100).toList()
assertThat(flowStartCount, `is`(1))
}
Is there a way to prevent the flow from restarting between first() and emitAll()?
I have a consumer that reads messages off MutableSharedFlow (which acts as an EventBus in my application). I am trying to write a unit test to show that passing a message into the Flow triggers my Listener.
This is my Flow definition:
class MessageBus {
private val _messages = MutableSharedFlow<Message>()
val messages = _messages.asSharedFlow()
suspend fun send(message: Message) {
_messages.emit(message)
}
}
Here is the Listener:
class Listener(private val messageBus: MessageBus) {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
init {
scope.launch {
messageBus.messages.collectLatest { message ->
when (message) {
is CustomMessage -> handleCustomMessage(message)
}
}
}
}
And finally here is my unit test:
class CommandTest {
#Test
fun `should process CustomMessage`(): Unit = runBlocking {
val messageBus = MessageBus()
val listener = Listener(messageBus)
messageBus.send(CustomMessage("test command"))
//argumentCaptor...verify[removed for brevity]
}
}
Unfortunately the above code does not trigger the break point in my Listener (breakpoint on line init is triggered, but a message is never received and no breakpoints triggered in the collectLatest block).
I even tried adding a Thread.sleep(5_000) before the verify statement but the result is the same. Am I missing something obvious with how coroutines work?
Edit: if it matters this is not an Android project. Simply Kotlin + Ktor
I imagine that since the code is in the init block in the Listener once you initialize val listener = Listener(messageBus, this) in the test it reads all messages and at this point you have none then in the next line you emit a message messageBus.send(CustomMessage("test command")) but your launch block should have finished by then. You can emit the message first or place your launch in an loop or in a different method that can be called after you emit the message
First of all I would recomend reading this article about how to test flows in Android.
Secondly in your example the issues arise from having the scope inside the Listener hardcoded. You should pass the scope as a parameter and inject it in the test:
class Listener(private val messageBus: MessageBus, private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()))
class CommandTest {
#Test
fun `should process CustomMessage`(): Unit = runBlockingTest {
val messageBus = MessageBus()
val listener = Listener(messageBus, this)
messageBus.send(CustomMessage("test command"))
//argumentCaptor...verify[removed for brevity]
}
}
I would also recomend using runBlockingTest instead of runBlocking so your tests don't have to actually wait. It will also fail in case any coroutines are left running once the test finishes.
You could use something like this
class Emitter {
private val emitter: MutableSharedFlow<String> = MutableSharedFlow()
suspend fun publish(messages: Flow<String>) = messages.onEach {
emitter.emit(it)
}.collect()
fun stream(): Flow<String> = emitter
}
the collect at the end of your onEach will be used to trigger the collection initially as a terminal operation... I need further understanding on emit because it does not work as I expect in all cases and when used in this way you have initially it does not post anything in your Flow unless you collect first to process
Then in your collector itself
class Collector {
suspend fun collect(emitter: Emitter): Unit = coroutineScope {
println("Starting collection...")
emitter.stream().collect { println("collecting message: $it") }
}
}
then your main (or test)
fun main() = runBlocking {
withContext(Dispatchers.Default + Job()) {
val emitter = Emitter()
val collector = Collector()
launch {
collector.collect(emitter)
}
emitter.publish(listOf("article#1", "article#2", "article#3", "article#4").asFlow())
}
}
output:
Starting collection...
collecting message: article#1
collecting message: article#2
collecting message: article#3
collecting message: article#4
I updated Kotlin coroutines to v1.6.0 and while addressing the changes in the tests I stumbled upon an issue when trying to test intermediate emissions. There's a section in the migration guide describing how to go about writing this kind of test, but the proposed solution is not fully working and I'd like to understand where's my mistake.
In particular, I have a StateFlow that gets updated twice in a coroutine launched in the viewModelScope (which internally uses Dispatchers.Main.immediate). However, when testing, the intermediate emission is not collected.
Simplified example:
class VM : ViewModel() {
val stateFlow = MutableStateFlow("a")
fun foo() {
viewModelScope.launch {
stateFlow.value = "b"
// [...] call to a suspend fun
stateFlow.value = "c"
}
}
}
class AbcTest {
#Test
fun testFlow() {
Dispatchers.setMain(UnconfinedTestDispatcher())
runTest {
val vm = VM()
val values = mutableListOf<String>()
val job = launch(UnconfinedTestDispatcher(testScheduler)) {
vm.stateFlow.collect(values::add)
}
vm.foo()
job.cancel()
assertEquals(listOf("a", "b", "c"), values)
}
Dispatchers.resetMain()
}
}
This test fails with: expected: <[a, b, c]> but was: <[a, c]>.
What am I missing?