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())
}
}
}
}
Related
I use hilt for the first in my dictionary app, but when I run my app, it crash and show this log:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: ir.arinateam.dictionary, PID: 23787
java.lang.RuntimeException: Cannot create an instance of class ir.arinateam.dictionary.feature_dictionary.presentation.WordInfoViewModel
at androidx.lifecycle.ViewModelProvider$NewInstanceFactory.create(ViewModelProvider.kt:204)
at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.kt:322)
at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.kt:304)
at androidx.lifecycle.SavedStateViewModelFactory.create(SavedStateViewModelFactory.kt:175)
at androidx.lifecycle.SavedStateViewModelFactory.create(SavedStateViewModelFactory.kt:138)
this is my module class:
`#Module
#InstallIn(SingletonComponent::class)
object WordInfoModule {
#Provides
#Singleton
fun provideGetWordInfoUseCase(repository: WordInfoRepository): GetWordInfo {
return GetWordInfo(repository)
}
#Provides
#Singleton
fun provideWordInfoDatabase(app: Application): WordInfoDatabase {
return Room.databaseBuilder(
app.applicationContext, WordInfoDatabase::class.java, "word_db"
).addTypeConverter(Converters(GsonParser(Gson())))
.build()
}
#Provides
#Singleton
fun provideWordInfoRepository(
db: WordInfoDatabase,
api: DictionaryApi
): WordInfoRepository {
return WordInfoRepositoryImpl(api, db.dao)
}
#Provides
#Singleton
fun provideDictionaryApi(): DictionaryApi {
return Retrofit.Builder()
.baseUrl(DictionaryApi.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(DictionaryApi::class.java)
}
}`
And this is my mainActivity class that init viewmodel and try to use it:
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var bindingActivity: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bindingActivity = DataBindingUtil.setContentView(this, R.layout.activity_main)
val viewModel: WordInfoViewModel by viewModels {
SavedStateViewModelFactory(application, this)
}
lifecycleScope.launch {
viewModel.eventFlow.collectLatest { event ->
when (event) {
is WordInfoViewModel.UIEvent.ShowSnackbar -> {
Snackbar.make(bindingActivity.root, event.message, Snackbar.LENGTH_SHORT)
.show()
}
}
}
}
}
}
And this is my ViewModel:
#HiltViewModel
class WordInfoViewModel #Inject constructor(
private val getWordInfo: GetWordInfo
) : ViewModel() {
private val _searchQuery = mutableStateOf<String>("")
val searchQuery: State<String> = _searchQuery
private val _state = mutableStateOf<WordInfoState>(WordInfoState())
val state: State<WordInfoState> = _state
private val _eventFlow = MutableSharedFlow<UIEvent>()
val eventFlow = _eventFlow.asSharedFlow()
private var searchJob: Job? = null
fun onSearch(query: String) {
_searchQuery.value = query
searchJob?.cancel()
searchJob = viewModelScope.launch {
delay(500L)
getWordInfo(query)
.onEach { result ->
when (result) {
is Resource.Success -> {
_state.value = state.value.copy(
wordInfoItems = result.data ?: emptyList(),
isLoading = false
)
}
is Resource.Error -> {
_state.value = state.value.copy(
wordInfoItems = result.data ?: emptyList(),
isLoading = false
)
_eventFlow.emit(UIEvent.ShowSnackbar(result.message ?: "Unknown Error"))
}
is Resource.Loading -> {
_state.value = state.value.copy(
wordInfoItems = result.data ?: emptyList(),
isLoading = true
)
}
}
}.launchIn(this)
}
}
sealed class UIEvent {
data class ShowSnackbar(val message: String) : UIEvent()
}
}
where is the problem and how can i solve it?
In your activity, you can now just use KTX viewModels() directly.
val viewModel: WordInfoViewModel by viewModels {
SavedStateViewModelFactory(application, this)
}
replace that part to
private val viewModel: MyViewModel by viewModels()
Hilt lets you to inject SavedStateHandle into viewmodels, you could use it as:
#HiltViewModel
class WordInfoViewModel #Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val getWordInfo: GetWordInfo
) : ViewModel() {
...
}
And in your activity:
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: WordInfoViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { ... }
}
If you feel comfortable with the still alpha library:
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03'
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.
I have following base class :
abstract class BaseViewModel<T, R>(private val schedulerProvider: BaseSchedulerProvider) :
ViewModel() {
private val compositeDisposable = CompositeDisposable()
private val _liveData = MutableLiveData<Resource<T>>()
val liveData: LiveData<Resource<T>>
get() = _liveData
protected abstract val requestObservable: Observable<R>
protected abstract fun getSuccessResult(it: R): T
fun sendRequest() {
_liveData.value = Resource.Loading()
composeObservable { requestObservable }
.subscribe({
_liveData.postValue(Resource.Success(getSuccessResult(it)))
}) {
_liveData.postValue(Resource.Failure(it.localizedMessage))
Timber.e(it)
}.also { compositeDisposable.add(it) }
}
}
And here is child class implementation :
class MainViewModel(
api: PokemonService,
schedulerProvider: BaseSchedulerProvider
) : BaseViewModel<List<Pokemon>, List<NamedResponseModel>>(schedulerProvider) {
override val requestObservable: Observable<List<NamedResponseModel>> =
api.getPokemonList(LIMIT).map { it.results }
override fun getSuccessResult(it: List<NamedResponseModel>): List<Pokemon> = it.asDomainModel()
init {
sendRequest()
}
}
As you see I put init block in child classes to sendRequest() which is a redundant. If I move init block to parent class, it will crash since api is null because init block of parent is called before constructor of child.
Is there any solution to move sendRequest() to parent and avoid redundant in child classes?
Source code can be found : https://github.com/AliRezaeiii/Pokemon
I think you need to change the design of your inheritance. To get the child items to be executed in the parent's initialization, you need to pass the object to the parent constructor.
Here is an example:
abstract class Base(protected val name: String) {
init {
println(name)
}
}
class CBase(private val s: String) : Base(s) {}
fun main() {
CBase("Hello");
}
In your case, which I haven't tested yet:
abstract class BaseViewModel<T, R>(
private val schedulerProvider: BaseSchedulerProvider,
protected val requestObservable: Observable<R>):
ViewModel() {
private val compositeDisposable = CompositeDisposable()
private val _liveData = MutableLiveData<Resource<T>>()
val liveData: LiveData<Resource<T>>
get() = _liveData
protected abstract fun getSuccessResult(it: R): T
fun sendRequest() {
_liveData.value = Resource.Loading()
composeObservable { requestObservable }
.subscribe({
_liveData.postValue(Resource.Success(getSuccessResult(it)))
}) {
_liveData.postValue(Resource.Failure(it.localizedMessage))
Timber.e(it)
}.also { compositeDisposable.add(it) }
}
init {
sendRequest()
}
}
class MainViewModel(
api: PokemonService,
schedulerProvider: BaseSchedulerProvider
) : BaseViewModel<List<Pokemon>, List<NamedResponseModel>>(
schedulerProvider,
api.getPokemonList(LIMIT).map { it.results }
) {
override fun getSuccessResult(it: List<NamedResponseModel>): List<Pokemon> = it.asDomainModel()
}
Here, you can still access the variable requestObservable at the parent's contructor because it is initialized at the constructor parameter, not as an abstract property.
Let me know how it works for you.
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.
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
}