Source code can be found at : https://github.com/AliRezaeiii/MVI-Architecture-Android-Beginners
I have following Unit test which is working fine :
@ExperimentalCoroutinesApi
@RunWith(MockitoJUnitRunner::class)
class MainViewModelTest {
@get:Rule
val rule: TestRule = InstantTaskExecutorRule()
@get:Rule
val coroutineScope = MainCoroutineScopeRule()
@Mock
lateinit var apiService: ApiService
@Mock
private lateinit var observer: Observer<MainState>
@Test
fun givenServerResponse200_whenFetch_shouldReturnSuccess() {
runBlockingTest {
`when`(apiService.getUsers()).thenReturn(emptyList())
}
val apiHelper = ApiHelperImpl(apiService)
val repository = MainRepository(apiHelper)
val viewModel = MainViewModel(repository, TestContextProvider())
viewModel.state.asLiveData().observeForever(observer)
verify(observer).onChanged(MainState.Users(emptyList()))
}
@Test
fun givenServerResponseError_whenFetch_shouldReturnError() {
runBlockingTest {
`when`(apiService.getUsers()).thenThrow(RuntimeException())
}
val apiHelper = ApiHelperImpl(apiService)
val repository = MainRepository(apiHelper)
val viewModel = MainViewModel(repository, TestContextProvider())
viewModel.state.asLiveData().observeForever(observer)
verify(observer).onChanged(MainState.Error(null))
}
}
The idea of unit test for stateFlow is taken from alternative solution in this question : Unit test the new Kotlin coroutine StateFlow
This is my ViewModel class :
@ExperimentalCoroutinesApi
class MainViewModel(
private val repository: MainRepository,
private val contextProvider: ContextProvider
) : ViewModel() {
val userIntent = Channel<MainIntent>(Channel.UNLIMITED)
private val _state = MutableStateFlow<MainState>(MainState.Idle)
val state: StateFlow<MainState>
get() = _state
init {
handleIntent()
}
private fun handleIntent() {
viewModelScope.launch(contextProvider.io) {
userIntent.send(MainIntent.FetchUser)
userIntent.consumeAsFlow().collect {
when (it) {
is MainIntent.FetchUser -> fetchUser()
}
}
}
}
private fun fetchUser() {
viewModelScope.launch(contextProvider.io) {
_state.value = MainState.Loading
_state.value = try {
MainState.Users(repository.getUsers())
} catch (e: Exception) {
MainState.Error(e.localizedMessage)
}
}
}
}
As you see when fetchUser() is called, _state.value = MainState.Loading
will be executed at start. As a result in unit test I expect following as well in advance :
verify(observer).onChanged(MainState.Loading)
Why unit test is passing without Loading
state?
Here is my sealed class :
sealed class MainState {
object Idle : MainState()
object Loading : MainState()
data class Users(val user: List<User>) : MainState()
data class Error(val error: String?) : MainState()
}
And here is how I observe it in MainActivity :
private fun observeViewModel() {
lifecycleScope.launch {
mainViewModel.state.collect {
when (it) {
is MainState.Idle -> {
}
is MainState.Loading -> {
buttonFetchUser.visibility = View.GONE
progressBar.visibility = View.VISIBLE
}
is MainState.Users -> {
progressBar.visibility = View.GONE
buttonFetchUser.visibility = View.GONE
renderList(it.user)
}
is MainState.Error -> {
progressBar.visibility = View.GONE
buttonFetchUser.visibility = View.VISIBLE
Toast.makeText(this@MainActivity, it.error, Toast.LENGTH_LONG).show()
}
}
}
}
}
Addendda: If I call userIntent.send(MainIntent.FetchUser)
method after viewModel.state.asLiveData().observeForever(observer)
instead of init
block of ViewModel, Idle
and Loading
states will be verified as expected by Mockito.