https://kotlinlang.org logo
#android
Title
# android
m

Michael Friend

07/08/2020, 3:09 PM
Im trying to test a ViewModel that lazily loads data when the LiveData is accessed and first emits a Loading state, the loads the data, then emits a Data state with the result. The data is loaded in a coroutine on the viewModelScope and I’m using TestCoroutineScope and MockK in my test but for some reason my mock observer doesn’t get the loading emission unless I pause the dispatcher then resume it. Here’s a basic example of my issue
Copy code
// The view model 
class LazyViewModel(val repo: Repo) : ViewModel() {
    val state: LiveData<State<String>> by lazy {
        loadData()
        _state
    }

    private val _state = MutableLiveData<State<String>>()
    private fun loadData() {
        viewModelScope.launch {
            _state.value = State.Loading
            _state.value = State.Data(repo.getData())
        }
    }
}
// the test
    @Test
    fun `get loading then data`() = coroutinesTestRule.testScope.runBlockingTest {
        val mockObserver: Observer<State<String>> = mockk(relaxUnitFun = true)

        val mockRepo: Repo = mockk {
            coEvery { getData() } returns "data"
        }
        val viewModel = LazyViewModel(mockRepo)
        // If I don't pause then resume the dispatcher _state.value = State.Loading seemingly gets skipped
        pauseDispatcher()
        viewModel.state.observeForever(mockObserver)
        resumeDispatcher()
        verifyOrder {
            mockObserver.onChanged(State.Loading)
            mockObserver.onChanged(State.Data("data"))
        }
    }
This is the failure i get. I don’t understand how the Loading state just doesn’t get received but pausing and resuming the dispatcher fixes it
m

Mgj

07/08/2020, 3:13 PM
I dont see where you've replaced your viewModelScope with a TestCoroutineScope...?
m

Michael Friend

07/08/2020, 3:21 PM
Is that the issue? Do you have to inject a CoroutineScope to ViewModels instead of using viewModelScope directly? My coroutine test rule sets main and follows the example here

https://youtu.be/KMb0Fs8rCRs?t=801

m

Mgj

07/08/2020, 3:24 PM
Im not sure if you have to but its what i've been doing and it seems to work 🤷 Instead of injecting it you could consider doing something like this:
Copy code
open class MyViewModel {
    protected open val mScope = CoroutineScope(Dispatchers.Main)
}
and then in tests use:
Copy code
class MyViewModelStub : MyViewModel {
	override val mScope = TestCoroutineScope()
}
m

Michael Friend

07/08/2020, 3:29 PM
So that just doesn't use the viewModelScope extension provided by android? I feel like there has to be a way to utilize androids extension
m

Mgj

07/08/2020, 3:29 PM
You can use viewModelScope if you want:
Copy code
open class MyViewModel {
    protected open val mScope = viewModelScope
}
m

Michael Friend

07/08/2020, 3:30 PM
Also something I've noticed is that if instead of calling loadData in the lazy block you call it manually from the test it works as expected
I figured out a way to make it work, but i still don’t quite understand why it wasn’t working before. If you use the liveData coroutine builder to get the lazy load then the test works as expected.
Copy code
val state: LiveData<State<String>> = liveData {
        // This HAS to be called first for the test to work
        emitSource(_state)
        loadData()
    }
    private val _state = MutableLiveData<State<String>>()
    
   private fun loadData() {
        viewModelScope.launch {
            _state.value = State.Loading
            _state.value = State.Data(repo.getData())
        }
    }
I initially avoided using the liveData build because I wanted to be able to allow retrying loadData so i needed the backing MutableLiveData. using emitSource(_state) instead of emit(Loading) and emit(Data) lets me do that still