Observe StateFlow as LiveData in Unit test - android

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.

Related

Coroutine advanceTimeBy function doesn't work

I have a simple viewModel:
class myViewModel: ViewModel() {
val someStringStateFlow = MutableStateFlow<String>("")
fun doSomething() {
viewModelScope.launch(DispatcherProvider.Main) {
delay(200)
someStringStateFlow.emit("Updated")
}
}
}
I have a test class:
class SearchViewModelTest : BehaviorSpec({
Dispatchers.setMain(DispatcherProvider(true).Main)
var viewModel = myViewModel()
Given("the view model is initialized") {
runTest {
When("call doSomeThing") {
viewModel.doSomething()
advanceUntilIdle()
advanceTimeBy(5000)
runCurrent()
Then("The stateflow should be updated") {
viewModel.someStringStateFlow.value shouldBe "Updated"
}
}
}
}
DispatcherProvider provides real or test dispatcher:
object DispatcherProvider {
private var isTest: Boolean? = null
operator fun invoke(isTest: Boolean? = null): DispatcherProvider {
this.isTest = isTest
return this
}
val Main: CoroutineDispatcher
get() = getTestDispatcher(isTest) ?: Dispatchers.Main
private fun getTestDispatcher(isTest: Boolean?): CoroutineDispatcher? =
if (isTest == true) testDispatcher else null
private val testDispatcher: CoroutineDispatcher by lazy {
newSingleThreadContext("Test Dispatcher")
}
}
This test should be successful but it fails. advanceTimeBy doesn't work correctly
What is wrong here?
I'm using coroutine version 1.6.4 with the new experimental test framework

How to make MockWebServer + Retrofit + Coroutines run in the same dispatcher

I'm unsuccessfully trying to use MockWebServer + Retrofit + Coroutines on my Android unit tests. During debugging, I found out that OkHttp is running on a different thread, which is why my test always fails.
This is my rule to set the test dispatcher:
class MainCoroutineRule(
private val scheduler: TestCoroutineScheduler = TestCoroutineScheduler(),
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(scheduler)
) : TestWatcher() {
val testScope = TestScope(testDispatcher)
override fun starting(description: Description) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description) {
super.finished(description)
Dispatchers.resetMain()
}
}
this is my ViewModel:
#HiltViewModel
class TestViewModel #Inject constructor(
private val repository: TestRepository,
#MainDispatcher private val dispatcher: CoroutineDispatcher
) : ViewModel() {
val hasData = MutableLiveData(false)
fun fetchSomething() {
viewModelScope.launch(dispatcher) {
when (repository.getSomething()) {
is Success -> {
hasData.value = true
}
else -> {
hasData.value = false
}
}
}
}
}
And finally the test:
class TestViewModelTest : MockServerSuite() {
#get:Rule
val taskExecutorRule = InstantTaskExecutorRule()
#get:Rule
val mainTestRule = MainCoroutineRule()
#Test
fun `my test`() = mainTestRule.testScope.runTest {
// setting up MockWebServer
val viewModel = TestViewModel(
repository = TestRepository(
api = testApi(server),
),
dispatcher = mainTestRule.testDispatcher
)
viewModel.fetchSomething()
assertThat(viewModel.hasData.value).isTrue
}
}
Since Retrofit is main-safe, I have no idea how to make it run on my testDispatcher. Am I missing something?
As Petrus mentioned, I already use getOrAwaitValue, which works for simple scenarios.
Our requests fire other requests after receiving some data in most of our use cases.
Usually, I receive intermediate values of getOrAwaitValue and not the final LiveData I was expecting.
Try to use getOrAwaitValue :)
class TestViewModelTest {
#Test
fun `my test`() = mainTestRule.testScope.runTest {
// setting up MockWebServer
val viewModel = TestViewModel(
repository = TestRepository(
api = testApi(server),
),
dispatcher = mainTestRule.testDispatcher
)
viewModel.fetchSomething()
assertThat(viewModel.hasData.getOrAwaitValue()).isTrue
}
}
#VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data = o
latch.countDown()
this#getOrAwaitValue.removeObserver(this)
}
}
this.observeForever(observer)
try {
afterObserve.invoke()
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
throw TimeoutException("LiveData value was never set.")
}
} finally {
this.removeObserver(observer)
}
#Suppress("UNCHECKED_CAST")
return data as T
}

Local unit test for LiveData while using RxJava

Full source code is available at : https://github.com/AliRezaeiii/StarWarsSearch-RxPaging
I am trying to test my DetailViewModel. My expectation is Species and Films not be empty lists as I have for instance : when(service.getSpecie(anyString())).thenReturn(Single.just(specie)). Here is my test :
class DetailViewModelTest {
#get:Rule
var rule: TestRule = InstantTaskExecutorRule()
#Mock
private lateinit var service: StarWarsService
private lateinit var specie: Specie
private lateinit var planet: Planet
private lateinit var film: Film
private lateinit var viewModel: DetailViewModel
#Before
fun setUp() {
initMocks(this)
// Make the sure that all schedulers are immediate.
val schedulerProvider = ImmediateSchedulerProvider()
val detailRepository = DetailRepository(service)
val character = Character(
"Ali", "127", "1385", emptyList(), emptyList()
)
viewModel = DetailViewModel(
schedulerProvider, character, GetSpecieUseCase(detailRepository),
GetPlanetUseCase(detailRepository), GetFilmUseCase(detailRepository)
)
specie = Specie("Ali", "Persian", "Iran")
planet = Planet("")
film = Film("")
}
#Test
fun givenServerResponse200_whenFetch_shouldReturnSuccess() {
`when`(service.getSpecie(anyString())).thenReturn(Single.just(specie))
`when`(service.getPlanet(anyString())).thenReturn(Single.just(planet))
`when`(service.getFilm(anyString())).thenReturn(Single.just(film))
viewModel.liveData.value.let {
assertThat(it, `is`(notNullValue()))
if (it is Resource.Success) {
it.data?.let { data ->
assertTrue(data.films.isEmpty())
assertTrue(data.species.isEmpty())
}
}
}
}
#Test
fun givenServerResponseError_whenFetch_specie_shouldReturnError() {
`when`(service.getSpecie(anyString())).thenReturn(Single.error(Exception("error")))
`when`(service.getPlanet(anyString())).thenReturn(Single.just(planet))
`when`(service.getFilm(anyString())).thenReturn(Single.just(film))
viewModel.liveData.value.let {
assertThat(it, `is`(notNullValue()))
if (it is Resource.Error) {
assertThat(it.message, `is`(notNullValue()))
assertThat(it.message, `is`("error"))
}
}
}
}
Here is my ViewModel :
class DetailViewModel #Inject constructor(
schedulerProvider: BaseSchedulerProvider,
character: Character,
getSpecieUseCase: GetSpecieUseCase,
getPlanetUseCase: GetPlanetUseCase,
getFilmUseCase: GetFilmUseCase,
) : BaseViewModel<DetailWrapper>(schedulerProvider,
Single.zip(Flowable.fromIterable(character.specieUrls)
.flatMapSingle { specieUrl -> getSpecieUseCase(specieUrl) }
.flatMapSingle { specie ->
getPlanetUseCase(specie.homeWorld).map { planet ->
SpecieWrapper(specie.name, specie.language, planet.population)
}
}.toList(),
Flowable.fromIterable(character.filmUrls)
.flatMapSingle { filmUrl -> getFilmUseCase(filmUrl) }
.toList(), { species, films ->
DetailWrapper(species, films)
}))
And here is my BaseViewModel :
open class BaseViewModel<T>(
private val schedulerProvider: BaseSchedulerProvider,
private val singleRequest: Single<T>
) : ViewModel() {
private val compositeDisposable = CompositeDisposable()
private val _liveData = MutableLiveData<Resource<T>>()
val liveData: LiveData<Resource<T>>
get() = _liveData
init {
sendRequest()
}
fun sendRequest() {
_liveData.value = Resource.Loading
wrapEspressoIdlingResourceSingle { singleRequest }
.subscribeOn(schedulerProvider.io())
.observeOn(schedulerProvider.ui()).subscribe({
_liveData.postValue(Resource.Success(it))
}) {
_liveData.postValue(Resource.Error(it.localizedMessage))
Timber.e(it)
}.also { compositeDisposable.add(it) }
}
override fun onCleared() {
super.onCleared()
compositeDisposable.clear()
}
}
And here is DetailWrapper class :
class DetailWrapper(
val species: List<SpecieWrapper>,
val films: List<Film>,
)
class SpecieWrapper(
val name: String,
val language: String,
val population: String,
)
Why films and species lists are empty in my local unit test?
As you see I pass two emptyLists to Character object. That is the source of problem since for instance I have following in DetailViewModel :
Flowable.fromIterable(character.filmUrls)
.flatMapSingle { filmUrl -> getFilmUseCase(filmUrl) }
.toList()
FilmUrls is one of those emptyLists. If I change Character by passing not emptyList, it is working as expected :
character = Character("Ali", "127", "1385",
listOf("url1", "url2"), listOf("url1", "url2"))
I also need to move ViewModel initialization to the method body, such as :
#Test
fun givenServerResponse200_whenFetch_shouldReturnSuccess() {
`when`(repository.getSpecie(anyString())).thenReturn(Single.just(specie))
`when`(repository.getPlanet(anyString())).thenReturn(Single.just(planet))
`when`(repository.getFilm(anyString())).thenReturn(Single.just(film))
viewModel = DetailViewModel(schedulerProvider, character, GetSpecieUseCase(repository),
GetPlanetUseCase(repository), GetFilmUseCase(repository))
viewModel.liveData.value.let {
assertThat(it, `is`(notNullValue()))
if (it is Resource.Success) {
it.data?.let { data ->
assertTrue(data.films.isNotEmpty())
assertTrue(data.species.isNotEmpty())
}
}
}
}

How to create unit test when u'r view model & repository return live data for Android

I already create some unit test for my view model. but when I println() the result it always return State Loading.. I have tried to read some article and cek in other source code but I'm still not found the answer.
Here is my code from ViewModel :
class PredefineViewModel() : ViewModel() {
private var predefineRepository: PredefineRepository? = PredefineRepository()
private val _predefined = MutableLiveData<String>()
val predefined: LiveData<Resource<Payload<Predefine>>> =
Transformations.switchMap(_predefined) {
predefineRepository?.predefine()
}
fun predefined() {
_predefined.value = "predefined".random().toString()
}
}
Here is my Repository
class PredefineRepository() {
private val api: PredefineApi? = PredefineApi.init()
fun predefine(): BaseMutableLiveData<Predefine> {
val predefine: BaseMutableLiveData<Predefine> = BaseMutableLiveData()
api?.let { api ->
predefine.isLoading()
api.predefined().observe()?.subscribe({ response ->
response?.let { resource ->
predefine.isSuccess(resource)
}
}, { error ->
predefine.isError(error)
})
}
return predefine
}
}
Here is my Resources State :
data class Resource<T>(var status: Status? = null, var meta: Meta? = null, var payload: T? =null) {
companion object {
fun <T> success(data: T?, meta: Meta): Resource<T> {
return Resource(Status.SUCCESS, meta, data)
}
fun <T> error(data: T?, meta: Meta): Resource<T> {
return Resource(Status.ERROR, meta, data)
}
fun <T> loading(data: T?, meta: Meta): Resource<T> {
return Resource(Status.LOADING, null, null)
}
}
}
UPDATE TEST CLASS
And, This is sample I try to print and check value from my live data view model :
class PredefineViewModelTest {
#get:Rule
val taskExecutorRule = InstantTaskExecutorRule()
private lateinit var viewModel: PredefineViewModel
private lateinit var repository: PredefineRepository
private lateinit var api: Api
#Before
fun setUp() {
api = Networks().bridge().create(Api::class.java)
repository = PredefineRepository()
viewModel = PredefineViewModel()
}
#Test
fun test_predefined(){
val data = BaseMutableLiveData<Predefine>()
val result = api.predefined()
result.test().await().assertComplete()
result.subscribe {
data.isSuccess(it)
}
`when`(repository.predefine()).thenReturn(data)
viewModel.predefined()
viewModel.predefined.observeForever {
println("value: $it")
println("data: ${data.value}")
}
}
}
UPDATE LOG Results
Why the result from my predefined always:
value: Resource(status=LOADING, meta=null, payload=null, errorData=[])
data: Resource(status=SUCCESS, meta=Meta(code=200, message=success, error=null), payload= Data(code=200, message=success, errorDara =[])
Thank You..
You would require to mock your API response. The unit test won't run your API actually you have to mock that. Please have a look at the attached snippet, It will give you a basic idea of how you can achieve that.
ViewModel:
class MainViewModel(val repository: Repository) : ViewModel() {
fun fetchData(): LiveData<Boolean> {
return Transformations.map(repository.getData()) {
if (it.status == 200) {
true
} else {
false
}
}
}
}
Repo:
open class Repository {
open fun getData() : LiveData<MainModel> {
return MutableLiveData(MainModel(10, 200))
}
}
Test Class:
#RunWith(MockitoJUnitRunner::class)
class MainViewModelTest {
lateinit var mainModel: MainViewModel
#Rule
#JvmField
var rule: TestRule = InstantTaskExecutorRule()
#Mock
lateinit var repo: Repository
init {
MockitoAnnotations.initMocks(this)
}
#Before
fun setup() {
mainModel = MainViewModel(repo)
}
#Test
fun fetchData_success() {
val mainModelData = MainModel(10, 200)
`when`(repo.getData()).thenReturn(MutableLiveData(mainModelData))
mainModel.fetchData().observeForever {
Assert.assertTrue(it)
}
}
#Test
fun fetchData_failure() {
val mainModelData = MainModel(10, 404)
`when`(repo.getData()).thenReturn(MutableLiveData(mainModelData))
mainModel.fetchData().observeForever {
Assert.assertFalse(it)
}
}
}
I couldn't see your API mock. Your initial status is loading inside LiveData.
{ response ->
response?.let { resource ->
predefine.isSuccess(resource)
}
block is not executing during the test.

UnitTest coroutines Kotlin usecase MVP

I am trying to mock a response from my usecases, this usecase works with coroutines.
fun getData() {
view?.showLoading()
getProductsUseCase.execute(this::onSuccessApi, this::onErrorApi)
}
My useCase is injected on presenter.
GetProductsUseCase has this code:
class GetProductsUseCase (private var productsRepository: ProductsRepository) : UseCase<MutableMap<String, Product>>() {
override suspend fun executeUseCase(): MutableMap<String, Product> {
val products =productsRepository.getProductsFromApi()
return products
}
}
My BaseUseCase
abstract class UseCase<T> {
abstract suspend fun executeUseCase(): Any
fun execute(
onSuccess: (T) -> Unit,
genericError: () -> Unit) {
GlobalScope.launch {
val result = async {
try {
executeUseCase()
} catch (e: Exception) {
GenericError()
}
}
GlobalScope.launch(Dispatchers.Main) {
when {
result.await() is GenericError -> genericError()
else -> onSuccess(result.await() as T)
}
}
}
}
}
This useCase call my repository:
override suspend fun getProductsFromApi(): MutableMap<String, Product> {
val productsResponse = safeApiCall(
call = {apiService.getProductsList()},
error = "Error fetching products"
)
productsResponse?.let {
return productsMapper.fromResponseToDomain(it)!!
}
return mutableMapOf()
}
Y try to mock my response but test always fails.
#RunWith(MockitoJUnitRunner::class)
class HomePresenterTest {
lateinit var presenter: HomePresenter
#Mock
lateinit var view: HomeView
#Mock
lateinit var getProductsUseCase: GetProductsUseCase
#Mock
lateinit var updateProductsUseCase: UpdateProductsUseCase
private lateinit var products: MutableMap<String, Product>
private val testDispatcher = TestCoroutineDispatcher()
private val testScope = TestCoroutineScope(testDispatcher)
#Mock
lateinit var productsRepository:ProductsRepositoryImpl
#Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
products = ProductsMotherObject.createEmptyModel()
presenter = HomePresenter(view, getProductsUseCase, updateProductsUseCase, products)
}
#After
fun after() {
Dispatchers.resetMain()
testScope.cleanupTestCoroutines()
}
//...
#Test
fun a() = testScope.runBlockingTest {
setTasksNotAvailable(productsRepository)
presenter.getDataFromApi()
verify(view).setUpRecyclerView(products.values.toMutableList())
}
private suspend fun setTasksNotAvailable(dataSource: ProductsRepository) {
`when`(dataSource.getProductsFromApi()).thenReturn((mutableMapOf()))
}
}
I don't know what is happening. The log says:
"Wanted but not invoked:
view.setUpRecyclerView([]);
-> at com.myProject.HomePresenterTest$a$1.invokeSuspend(HomePresenterTest.kt:165)
However, there was exactly 1 interaction with this mock:
view.showLoading();"
The problem is with how you create your GetProductsUseCase.
You're not creating it with the mocked version of your ProductsRepository, yet you're mocking the ProductsRepository calls.
Try to create the GetProductsUseCase manually and not using a #Mock
// no #Mock
lateinit var getProductsUseCase: GetProductsUseCase
#Before
fun setUp() {
// ...
// after your mocks are initialized...
getProductsUseCase = GetProductsUseCase(productsRepository) //<- this uses mocked ProductsRepository
}

Categories

Resources