Hey guys, I'm trying to do something with Orbit's ...
# orbit-mvi
b
Hey guys, I'm trying to do something with Orbit's testing framework and basically the test finishes too early and so my assertion fails. I'm trying to simulate an empty state in a Repository's Flow that should trigger a refresh, here's the code:
Copy code
class MyContainerHost(
    scope: CoroutineScope,
    val repositoryFlow: Flow<String?>, // Flow representing repository state
    val requestRefresh: () -> Unit
) :
    ContainerHost<String, String> {
    override val container: Container<String, String> =
        scope.container("initial") {
            // When creating the container, listen for update on the Repository flow
            scope.launch {
                repositoryFlow.collect { newState ->
                    intent {
                        // When no state in flow, refresh it
                        if (newState != null) {
                            reduce { newState }
                        } else {
                            requestRefresh()
                        }
                    }
                }
            }
        }
}
The thing is when I write a unit test for that, the test returns before being able to get into my
collect{}
block. How would you deal with that? Thanks
The test code:
Copy code
val repositoryFlow = MutableStateFlow<String?>(null)
            val containerHost = MyContainerHost(
                this,
                repositoryFlow = repositoryFlow,
                requestRefresh = {
                    // This does not get called
                    repositoryFlow.tryEmit("refreshed state")
                }
            ).test("initial")

            containerHost.runOnCreate()

            containerHost.assert("initial") {
                states(
                    // This gives "expected states but never received"
                    { "refreshed state" }
                )
            }
m
@Benoît I think the problem here is that you’re launching the flow collection from the raw origin scope - Orbit’s testing framework cannot handle coroutines launched outside of it’s own scope. My suggestion would be to collect the flow within
intent
instead. No need for launching an extra new coroutine, as that’s what
intent
does anyway.
please let me know if that works
So something like:
Copy code
class MyContainerHost(
    scope: CoroutineScope,
    val repositoryFlow: Flow<String?>, // Flow representing repository state
    val requestRefresh: () -> Unit
) :
    ContainerHost<String, String> {
    override val container: Container<String, String> =
        scope.container("initial") {
            // When creating the container, listen for update on the Repository flow
                intent {
                    repositoryFlow.collect { newState ->
                        // When no state in flow, refresh it
                        if (newState != null) {
                            reduce { newState }
                        } else {
                            requestRefresh()
                        }
                    }
                }
            }
        }
}
b
Thanks Mikolaj, this doesn't work because the intent does not return (stuck doing collect) so the test gets stuck and eventually times out
m
Tests that test
ContainerHost
using `Flow`s need special treatment. There are a few options here I think. 1. Provide a finite flow as
repositoryFlow
2. You can
runOnCreate
in your test in a launched coroutine so it doesn’t block your test 3. You can use
liveTest
I think 1) is the easiest and the most preferable here
If your flow naturally completes, this
intent
will finish and the test can carry on.
Let me know if that works!
b
1. The problem with this approach is that I simplified the test for the example, but I'm actually testing a more complex feature that requires me to have a
MutableStateFlow
so I can send a bunch of stuff at different moment in time 2. This caused "expected states but never received", which makes sense, the
runOnCreate
doesn't get enough time to run before the assertion 3. What do you mean liveTest? How do I do that?
m
So a bit of background: The default suspending testing mode was developed to improve testing performance. It isolates the code under test (the invoked intent) from the rest of the framework and runs it within the test method as a simple suspending function. However, you can also test using the real container as-is, with only one dispatcher swapped out (which you can override anyway). Use
ContainerHost.liveTest
for this. The assertions still work - they await for emissions from state and side effect flows with a timeout. So you can treat the rest of the test the same I think.
I think in your case
liveTest
will work best @Benoît
hope this helps
b
Ah perfect!
liveTest
is what I needed this whole time, thanks a lot @Mikolaj Leszczynski! Btw there's no mention of
liveTest
on https://orbit-mvi.org/Test/overview/, might be a good idea to add it
m
Yes indeed, mea culpa 😅
r
does any of the above change with the new
runTest
?
b
It shouldn't, although in my experience
runTest
isn't strictly equal to
runBlocking
, if a test passes with
runBlocking
you can't (always) simply change it to
runTest
and expect it to pass as well. Sometimes the order of execution can differ