I'm trying to update to v10 and running into a wei...
# orbit-mvi
j
I'm trying to update to v10 and running into a weird issue in my unit tests. I have a ViewModel that is collecting a flow on initialization, but the unit test never seems to receive the state after the reducer is called. Am I doing it wrong, or is this something weird with v10? LoginViewModel.kt
Copy code
class LoginViewModel(
    private val authController: AuthenticationController,
) : ViewModelContainerHost<LoginState, LoginSideEffect>() {
    override val container: Container<LoginState, LoginSideEffect> =
        container(
            initialState = LoginState.UNINITIALIZED,
        ) {
            observeAuthState()
        }

    private fun observeAuthState() =
        intent(registerIdling = false) {
            repeatOnSubscription {
                authController.authState
                    .collect { state ->
                        reduce {
                            when (state) {
                                AuthState.UNINITIALIZED -> LoginState.UNINITIALIZED
                                AuthState.UNAUTHORIZED -> LoginState.UNAUTHORIZED
                                AuthState.AUTHORIZED -> LoginState.AUTHORIZED
                                AuthState.ERROR -> LoginState.ERROR
                            }
                        }
                    }
            }
        }
}
LoginViewModelTests.kt
Copy code
class LoginViewModelTests {
    val authService =
        mock<AuthService>(MockMode.strict) {
        }

    @Test
    fun testConstructorAuthorized() =
        runTest {
            everySuspend { authService.isAuthorized() } returns true

            val authController = AuthenticationController(Logger, authService)
            LoginViewModel(authController).test(this, LoginState.UNINITIALIZED) {
                runOnCreate()
                expectState(LoginState.AUTHORIZED)
            }
        }
}
Result:
Copy code
Info: Updating auth state to AUTHORIZED

Timed out waiting for remaining intents to complete for 1s
org.orbitmvi.orbit.test.OrbitTimeoutCancellationException: Timed out waiting for remaining intents to complete for 1s
...
The weird thing is that if I
expectState(LoginState.UNAUTHORIZED)
, then it fails because it gets
AUTHORIZED
.
βœ… 1
a
hey @Jacob Rhoda, out of curiousity what version were you updating from?
if i remember correctly, there was a change in either v8 or v9 to how the creation block runs. originally this used to be just a function but at some point we updated it to be an intent block. having looked at your code, what this then means is effectively your
observeAuthState()
function is nesting
intent
blocks. this ends up being bad, one for testing as you're experiencing but also from a structured concurrency point of view - it is like nesting
viewModelScope.launch
. i think ultimately we need to re-write some of our documentation around repeatOnSubscription as the example feels mis-leading. in terms of solving, the simplest would be to replace intent with subIntent - subIntent uses the same coroutine scope. however, its worth noting if you have espresso tests then currently there's no way to disable idling resource support
j
Hi Matthew, thanks for responding to my question. I updated from v9 but the
observeAuthState
intent might be a holdover from v8. I have been a bit confused by how to collect flows β€” I didn't realize that the creation block was an intent. I also am unclear what the idling resource is intended to do β€” your docs have an example both with it, and without it, and the API docs don't make it clear exactly what that parameter does. (Not trying to complain β€” just giving feedback.) In terms of changing it from an
intent
to a
subIntent
I did try that, but to no avail. (It's a normal unit test, not an espresso test.) If I add
println
to tell me when different closures are being called, it indicates that the
reduce
function is called. But then the test never receives the
AUTHORIZED
state and times out.
Copy code
class LoginViewModel(
    private val authController: AuthenticationController,
) : ViewModelContainerHost<LoginState, LoginSideEffect>() {
    override val container: Container<LoginState, LoginSideEffect> =
        container(
            initialState = LoginState.UNINITIALIZED,
        ) {
            println("Container created")
            observeAuthState()
        }

    private suspend fun observeAuthState() =
        subIntent {
            println("Sub intent started")
            authController.authState
                .collect { state ->
                    println("Received auth state $state")
                    reduce {
                        println("Reducing for auth state $state")
                        when (state) {
                            AuthState.UNINITIALIZED -> LoginState.UNINITIALIZED
                            AuthState.UNAUTHORIZED -> LoginState.UNAUTHORIZED
                            AuthState.AUTHORIZED -> LoginState.AUTHORIZED
                            AuthState.ERROR -> LoginState.ERROR
                        }
                    }
                }
        }
Copy code
Creating auth controller
Info: Updating auth state to AUTHORIZED
Creating login view model
Testing login view model
Running on create
Expecting state UNINITIALIZED
Received state UNINITIALIZED
Expecting state AUTHORIZED
Container created
Sub intent started
Received auth state AUTHORIZED
Reducing for auth state AUTHORIZED
m
@Jacob Rhoda from a quick look at this it looks to me like your intent collecting the flow is still running at the end of the test. After executing the test block, Orbit ensures there are no intents still running (i.e. no possibility of receiving further states that we're not expecting). This is the error you're getting. You have a few options to fix it:
1. Use a finite flow for your
authController.authState
- the intent will naturally complete once it completes 2. Use
subIntent
like you're doing here. Capture the job :
val job = runOnCreate()
and
job.cancel
it at the end of the test. 3. Use
cancelAndIgnoreRemainingItems()
at the end of the test
πŸ‘ 1
We are working on a new API to make collecting and testing flows in your
ContainerHost
easier and more consistent
j
I will try these. Thank you!!!
πŸ‘ 1
m
Did that work @Jacob Rhoda?
j
@Mikolaj Leszczynski Thank you, sir. Those solutions work. I can't do (1) because it's meant to be an infinite flow. But cancelling the job works!
πŸ‘ 1
m
Glad to have helped! Like I said we're bouncing around a few ideas how to streamline working with Flows in Orbit πŸ˜‰
j
I appreciate that. There are two examples in the docs and both are different, and the difference isn't explained. πŸ˜‰ (I'm an iOS developer learning this Kolin/Android thing, so I'm not 100% familiar with how Coroutines does things.) Specifically, there's an example in the section on Container factories and Repeat on subscription which collect flows differently.
m
Thanks for the tip, we'll review the docs when we complete this feature