Given I call some async rx stuff in the view model...
# android-architecture
u
Given I call some async rx stuff in the view model constructor, and usual recommendation to have synchronous schedulers swapped in for testing, wouldnt this mean that I can never assert State.Loading? (Since Loading is emitted synchronously, so is then Success, now ctor is exited, and now I can test the state observable, meaning atleast I would miss Loading)
s
You can test loading states and such. Say your ViewModel uses a data-source to get data from a remote server. Provide and inject a test version of that data-source. Its methods that emit data can be mocked and delays can be mocked by Rx
delay
calls (be sure to use the version that takes a
scheduler
). Then call testScheduler.advanceTimeBy/To to advance (virtual) time, last the mock-delays, mimicking Loading state.
u
I mean sure, if you introduce asynchrony then fine. Why is immediate sched. used then in tests
But is there a reason to introduce the asynchrony? I think receiving Loading and then Success synchronously for testing is fine. Issue is only that you need to subscribe to stateObservable first to catch both values.
which is impossible if the loading observable is subscribed to in viewmodel constructor
it there was a explicit init function, it would all work
Copy code
val viewModel = createViewModel()
val testObserver = viewModel.stateObservable.test()
viewModel.init()
testObserver.assertValues(...)
k
i normally don’t consider Loading state in viewModel
i consider it part of view (activity/fragment)
consider a scenario of Login, when user press Login then view knows to show progress so view directly call showProgress() and whenever viewModel update view via livedata about success/failure of login then view simply hides loading state via hideProgress()
“Given I call some async rx stuff in the view model constructor” instead of using constructor for async rx stuff, i will suggest to use lazy loading of liveData which will be on demand loading of async rx stuff
Consider a scenario where when user opens a order screen then app needs to fetch order list from server ViewModel will be like:
Copy code
val orderInfoList by lazy {
        val liveData = MutableLiveData<OrderListResult>()
        loadingState.value = LoadingState.LOADING
        
        val params = GetOrderInfoUseCase.Parameters(STATUS)
        getOrderListUseCase.execute(object : FlowableSubscriber<Result<List<OrderInfo>>>() {
            override fun onNext(t: Result<List<OrderInfo>>?) {
                TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
                loadingState.value = LoadingState.STOP_LOADING
            }

            override fun onError(t: Throwable?) {
                TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
                loadingState.value = LoadingState.STOP_LOADING
            }
        }, params)
        addDisposable(getOrderListUseCase)

        return@lazy liveData
    }
        }

        override fun error(error: RetrofitError) {
            logger.e(TAG, "get order list error occurred", error.errorMessage)

            if (error.isNetworkError()) {
                orderInfoList.value = OrderListResult.Error(resourceProvider.getString(R.string.network_unavailable))
            } else {
                orderInfoList.value = OrderListResult.Error(error.errorMessage)
            }
            checkAndGetInProgressOrders()
        }
    }, params)
    addDisposable(getOrderListUseCase)

    return@lazy liveData
}
u
Sorry I dont follow, why wouldnt Loading be part of view model State? regardless if its a single state class or partial live datas
k
“my way” is Loading State handle by view but i also share Loading State in viewModel in above code for your easness of understanding
d
Big sorry, but I don't understand the problem entirely, can you elaborate a bit and show a full test case? Normally testing should be easy. Register a mock observer and verify that its called with the right argument:
Copy code
@Test
    fun `Should reset the color of the info button when showing a valid info`() {
        // GIVEN
        val observer: (Int) -> Unit = mock()

        viewModel.colorInfoButton.observeForever(
                Observer(observer))

        // WHEN
        gameCommands.emit(ShowInfo(validInfo()))

        // THEN
        verify(observer).invoke(WHITE)
    }
Don't forget
Copy code
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
though, otherwise the test case crashes
u
@Daniel issue is if you "command" happens in constructor, i.e. load something on view model creation
and it happens synchronously, and you cannot attach your observer as you do there soon enough
d
Ahh now I understand! Thank you. You can fall back to
assertThat(viewModel.liveData.value).isEqualTo(expectedValue)
Only sensible if you use multiple live data tough. If you use only one and push events to the view like this you can only assert the last one send 😕
k
in that case you can use lazyLoading instead of triggering event on viewModel constructor
s
LiveData
are like BehaviorSubjects (Rx). They are statefull. You can start your "command" in the constructor/init of your ViewModel (which happens during an onCreate) and attach/observe a bit later. If the "command" finishes very quickly, the value from the LiveData will be emitted to the observer immediately as soon as the oberver subscribes/attaches. If the "command" finishes later, the oberver attaches and as soon as the command finishes at a later time, the result is emitted to the observer.
u
not really, because BehaviourSubject only caches 1 value
therefore if you change your schedulers to be synchronous, youll emit Loading, Success synchronously, and only Success will be cached in the subject and you cannot assert Loading happend
which is the issue, unless you attach the testObserver sooner, then run the constructor, and then assert the testObserver saw Loading, Success
but you cannot do that obviously with ctors, so I switched to explicit fun init(), and no problems no, I like it
tldr; doing stuff in ctors is stupid for testing
(unless you swap behaviour relay for full replay relay etc) but its more test specific config since now you need a overridable function which is counter productive imo
Copy code
@Test fun `after view model init() called, await these refresh Signal values`() {
        whenever(messageManager.refresh()).thenReturn(Observable.just(Foo(77)))
        val initialState = InviteViewModel.State(refresh = Uninitialized)

        val viewModel = createInviteViewModel(initialState)
        val testSubscriber = viewModel.stateObservable.map { it.refresh }.test()

        viewModel.init()

        testSubscriber.awaitAndAssertValues(
            Uninitialized,
            Loading,
            Success(Foo(77))
        )
    }
so the explicig init() maps directly to any other method like changeTitle()
Copy code
@Test fun `meh asd`() {
        val initialState = InviteViewModel.State(title = "")

        val viewModel = createInviteViewModel(initialState)
        val testSubscriber = viewModel.stateObservable.map { it.title }.test()

        viewModel.changeTitle()

        testSubscriber.awaitAndAssertValues(
            "",
            "Foo"
        )
    }
win win, what do you guys think?
s
Why do your schedulers to be synchronous? Also, why is
Success
emitted even before the constructor has finished? Your view-model’s constructor could take an initial state (e.g Loading, since that would be the first thing the user sees) and a data-repository. The view-model sets its state to the initial state and you could assert that that state is Loading.
Copy code
whenever(repo.getTitle()).thenReturn(Single.just("New Title"))

viewModel = MyViewModel(repo, State.Loading)
val testSub = viewModel.stateObs.test()

testSub.assertValue(State.Loading)

viewModel.changeTitle() // will call repo.getTitle() which returns a Single<String>

testSub.assertValues(State.Loading, State.Success("New Title")) // or something similar
If the ViewModel starts loading/fetching a new title right in its constructor, you can delay it in your repo.
Copy code
whenever(repo.getTitle()).thenReturn(Single.just("New Title").delay(1, TimeUnit.SECONDS, testScheduler))

viewModel = MyViewModel(repo, State.Loading)
val testSub = viewModel.stateObs.test()

testSub.assertValue(State.Loading)

testScheduler.advanceTimeBy(1, TimeUnit.SECONDS)

testSub.assertValues(State.Loading, State.Success("New Title")) // or something similar
u
well sure timeScheduler, but why? this delay is indeterminate in practise, id find synchronous Trampoline to be cleaner, no need to mess around with advancing time