2

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.

Ali
  • 9,800
  • 19
  • 72
  • 152
  • You can find answer of this question here : https://stackoverflow.com/questions/65388332/right-place-to-offer-or-send-channel-in-mvi-pattern – Ali Jan 02 '21 at 12:19

0 Answers0