Search code examples
androidunit-testingrx-javarx-java2

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?


Solution

  • 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())
                    }
                }
            }
        }