I have a ViewModel that talks to a use case and gets a flow back i.e Flow<MyResult>. I want to unit test my ViewModel. I am new to using the flow. Need help pls. Here is the viewModel below -
class MyViewModel(private val handle: SavedStateHandle, private val useCase: MyUseCase) : ViewModel() {
private val viewState = MyViewState()
fun onOptionsSelected() =
useCase.getListOfChocolates(MyAction.GetChocolateList).map {
when (it) {
is MyResult.Loading -> viewState.copy(loading = true)
is MyResult.ChocolateList -> viewState.copy(loading = false, data = it.choclateList)
is MyResult.Error -> viewState.copy(loading = false, error = "Error")
}
}.asLiveData(Dispatchers.Default + viewModelScope.coroutineContext)
MyViewState looks like this -
data class MyViewState(
val loading: Boolean = false,
val data: List<ChocolateModel> = emptyList(),
val error: String? = null
)
The unit test looks like below. The assert fails always don't know what I am doing wrong there.
class MyViewModelTest {
@get:Rule
val instantExecutorRule = InstantTaskExecutorRule()
private val mainThreadSurrogate = newSingleThreadContext("UI thread")
private lateinit var myViewModel: MyViewModel
@Mock
private lateinit var useCase: MyUseCase
@Mock
private lateinit var handle: SavedStateHandle
@Mock
private lateinit var chocolateList: List<ChocolateModel>
private lateinit var viewState: MyViewState
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
Dispatchers.setMain(mainThreadSurrogate)
viewState = MyViewState()
myViewModel = MyViewModel(handle, useCase)
}
@After
fun tearDown() {
Dispatchers.resetMain() // reset main dispatcher to the original Main dispatcher
mainThreadSurrogate.close()
}
@Test
fun onOptionsSelected() {
runBlocking {
val flow = flow {
emit(MyResult.Loading)
emit(MyResult.ChocolateList(chocolateList))
}
Mockito.`when`(useCase.getListOfChocolates(MyAction.GetChocolateList)).thenReturn(flow)
myViewModel.onOptionsSelected().observeForever {}
viewState.copy(loading = true)
assertEquals(viewState.loading, true)
viewState.copy(loading = false, data = chocolateList)
assertEquals(viewState.data.isEmpty(), false)
assertEquals(viewState.loading, true)
}
}
}
I think I have found a better way to test this, by using Channel and consumeAsFlow extension function. At least in my tests, I seem to be able to test multiple values sent throught the channel (consumed as flow).
So.. say you have some use case component that exposes a Flow<String>. In your ViewModelTest, you want to check that everytime a value is emitted, the UI state gets updated to some value.
In my case, UI state is a StateFlow, but this should be do-able with LiveData as well.
Also, I am using MockK, but should also be easy with Mockito.
Given this, here is how my test looks:
@Test
fun test() = runBlocking(testDispatcher) {
val channel = Channel<String>()
every { mockedUseCase.someDataFlow } returns channel.consumeAsFlow()
channel.send("a")
assertThat(viewModelUnderTest.uiState.value, `is`("a"))
channel.send("b")
assertThat(viewModelUnderTest.uiState.value, `is`("b"))
}
EDIT: I guess you can also use any kind of hot flow implementation instead of Channel and consumeAsFlow. For example, you can use a MutableSharedFlow that enables you to emit values when you want.