Testing android fragments with activityViewModels - android

I'm trying to run android instrumentation tests for the fragments that share the same ViewModel. The ViewModel have some arguments.
I use koin, but when using activityViewModels in the tests koin does not inject the ViewModel.
class SomeFragment() : Fragment() {
private val viewModel: SomeViewModel by activityViewModels()
... more code
}
class SomeFragmentTest() : KoinTest{
#Before
fun setup() {
val viewModel: SomeViewModel = mockk(relaxed = true)
startKoin { loadKoinModules(listOf(
module(override = true) { viewModel },
module(override = true) { factory { appAnalytics } })) }
}
.... more code
}
The message received is
Caused by: java.lang.InstantiationException: java.lang.Class<SomeViewModel> has no zero argument constructor

I found a solution, and I posted this question and I hope it helps others.
I added a view model factory this to the fragment:
class SomeFragment(val factory: (() -> ViewModelProvider.Factory)? = null) : Fragment() {
private val viewModel: SomeViewModel by activityViewModels(factory)
... more code
}
class SomeFragmentTest() : KoinTest{
private val fragmentFactory : FragmentFactory = mockk()
#Before
fun setup() {
val viewModel: SomeViewModel = mockk(relaxed = true)
... some mock related to viewModel
val viewModelFactory : ViewModelProvider.Factory = mockk()
every { viewModelFactory.create(SomeViewModel::class.java) } answers { viewModel }
every { fragmentFactory.instantiate(any(), any()) } answers { SomeFragment{
viewModelFactory
}}
}
#Test
fun test_fragment() {
launchFragmentInContainer<SomeFragment>(
themeResId = R.style.AppTheme,
factory = fragmentFactory
)
... some asserts
}
.... more code
}
This works for prod because in the activityViewModels implementation when the factory is null the default is used:
inline fun <reified VM : ViewModel> Fragment.activityViewModels(
noinline factoryProducer: (() -> Factory)? = null
) = createViewModelLazy(VM::class, { requireActivity().viewModelStore },
factoryProducer ?: { requireActivity().defaultViewModelProviderFactory })

Related

AndroidTest org.koin.core.error.NoBeanDefFoundException: No definition found for class

I am trying to mock viewmodel in mu android test but i am getting this error
org.koin.core.error.NoBeanDefFoundException: No definition found for
class:'com.example.berlinclockdemo.ui.MainActivityViewModel'. Check
your definitions!
How can I solve this error? or is there any alternate way of doing this. All I want
to do is manipulate viewmodel live data so that I can check my UI for my desired data
EX: Recyclerview data needs to show what my expected data is.
Below is my code
My ViewModel
MainActivityViewModel:
class MainActivityViewModel(private val berlinClock: BerlinClock) : ViewModel() {
var berlinClockTime = MutableLiveData<BerlinClockData>()
val berlinClockValue: LiveData<BerlinClockData> = berlinClockTime
fun updateBerlinClock(time: String) {
berlinClockTime.postValue(berlinClock.getBerlinClock(time))
}
}
My Test Activity
MainAvtivityTest:
#RunWith(AndroidJUnit4::class)
#LargeTest
class MainActivityKakaoTest : KoinTest {
#Rule
#JvmField
val rule = ActivityScenarioRule(MainActivity::class.java)
private val mockedBerlinClock: BerlinClock = mockk()
private val appModule = module {
single { mockedBerlinClock }
}
private val viewModelModule = module {
viewModel {
MainActivityViewModel
(get())
}
}
private val viewModel: MainActivityViewModel by inject()
#Before
fun setUp() {
MockKAnnotations.init(this, relaxUnitFun = true)
val expectedBerlinTime =
(getBerlinClockData(arrayOf("RRRR RRRR", "YYROOOOOOOO YYOO", "O")))
loadKoinModules(
listOf(
appModule, viewModelModule
)
)
every {
mockedBerlinClock.getBerlinClock(
SimpleDateFormat(
MainActivity.TIME_FORMAT,
Locale.getDefault()
).format(Date())
)
} returns expectedBerlinTime
viewModel.updateBerlinClock(
SimpleDateFormat(MainActivity.TIME_FORMAT, Locale.getDefault()).format(
Date()
)
)
assertThat(expectedBerlinTime).isEqualTo(viewModel.berlinClockValue.value)
}
}
TestApplication:
class TestApp : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger(Level.NONE)
androidContext(this#TestApp)
modules(emptyList())
}
}
}
TestRunner:
class TestAppJUnitRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, className: String?, context:
Context?): Application {
return super.newApplication(cl, TestApp::class.java.name, context)
}
}

Android Unit Test ViewModel Wanted but not invoked

I'm new on unit testing. I'm trying to do unit testing on my view model class but my test fail with error:
Wanted but not invoked:
toggleMovieFavorite.invoke(
Movie(id=1, title=Title, overview=Overview, releaseDate=01/01/2025, posterPath=, backdropPath=, originalLanguage=ES, originalTitle=Title, popularity=5.0, voteAverage=7.0, favorite=false)
);
-> at xyz.jonthn.usescases.ToggleMovieFavorite.invoke(ToggleMovieFavorite.kt:7)
Actually, there were zero interactions with this mock.
This is my test file
#RunWith(MockitoJUnitRunner::class)
class DetailViewModelTest {
#get:Rule
val rule = InstantTaskExecutorRule()
#Mock
lateinit var findMovieById: FindMovieById
#Mock
lateinit var toggleMovieFavorite: ToggleMovieFavorite
#Mock
lateinit var observer: Observer<Movie>
private lateinit var vm: DetailViewModel
#ExperimentalCoroutinesApi
#Before
fun setUp() {
Dispatchers.setMain(Dispatchers.Unconfined)
vm = DetailViewModel(1, findMovieById, toggleMovieFavorite, Dispatchers.Unconfined)
}
#ExperimentalCoroutinesApi
#After
fun tearDown() {
Dispatchers.resetMain()
}
#Test
fun `when favorite clicked, the toggleMovieFavorite use case is invoked`() {
runBlocking {
val movie = mockedMovie.copy(id = 1)
whenever(findMovieById.invoke(1)).thenReturn(movie)
whenever(toggleMovieFavorite.invoke(movie)).thenReturn(movie.copy(favorite = !movie.favorite))
vm.movie.observeForever(observer)
vm.onFavoriteClicked()
verify(toggleMovieFavorite).invoke(movie)
}
}
val mockedMovie = Movie(
0,
"Title",
"Overview",
"01/01/2025",
"",
"",
"ES",
"Title",
5.0,
7.0,
false)
}
This is my DetailViewModel:
class DetailViewModel(
private val movieId: Int, private val findMovieById: FindMovieById,
private val toggleMovieFavorite: ToggleMovieFavorite,
uiDispatcher: CoroutineDispatcher) : ScopedViewModel(uiDispatcher) {
private val _movie = MutableLiveData<Movie>()
val movie: LiveData<Movie> get() = _movie
init {
launch {
_movie.value = findMovieById.invoke(movieId)
}
}
fun onFavoriteClicked() {
launch {
movie.value?.let {
_movie.value = toggleMovieFavorite.invoke(it)
}
}
}
}
And my use case ToggleMovieFavorite:
class ToggleMovieFavorite(private val moviesRepository: MoviesRepository) {
suspend fun invoke(movie: Movie): Movie = with(movie) {
copy(favorite = !favorite).also { moviesRepository.update(it) }
}
}
Thank you so much for your help guys!!!
i thougt mockito does not invoke your init method on viewmodel, you should put your declaration of vm on each #test instead of #Before since method findMovieById called at init, right before the function is mocked.

CoroutineLiveData Builder repository not being invoked

I'm trying to use the new liveData builder referenced here to retrieve my data, then transform it into view models. However, my repository code isn't being invoked (at least I'm not able to see it being triggered when I use my debugger). Am I not supposed to use two liveData{ ... } builders? (one in my repository, one in my view model)?
class MyRepository #Inject constructor() : Repository {
override fun getMyContentLiveData(params: MyParams): LiveData<MyContent> =
liveData {
val myContent = networkRequest(params) // send network request with params
emit(myContent)
}
}
class MyViewModel #Inject constructor(
private val repository: MyRepository
) : ViewModel() {
val viewModelList = liveData(Dispatchers.IO) {
val contentLiveData = repository.getContentLiveData(keyParams)
val viewModelLiveData = contentToViewModels(contentLiveData)
emit(viewModelLiveData)
}
private fun contentToViewModels(contentLiveData: LiveData<MyContent>): LiveData<List<ViewModel>> {
return Transformations.map(contentLiveData) { content ->
//perform some transformation and return List<ViewModel>
}
}
}
class MyFragment : Fragment() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
val myViewModel: MyViewModel by lazy {
ViewModelProviders.of(this, viewModelFactory).get(MyViewModel::class.java)
}
lateinit var params: MyParams
override fun onAttach(context: Context) {
AndroidSupportInjection.inject(this)
super.onAttach(context)
myViewModel.params = params
myViewModel.viewModelList.observe(this, Observer {
onListChanged(it)
})
}
You could try with the emitSource:
val viewModelList = liveData(Dispatchers.IO) {
emitSource(
repository.getContentLiveData(keyParams).map {
contentToViewModels(it)
}
}

Correct structure of implementing MVVM LiveData RxJava Dagger Databinding?

MainActivity
class MainActivity : AppCompatActivity() {
#Inject
lateinit var mainViewModelFactory: mainViewModelFactory
private lateinit var mainActivityBinding: ActivityMainBinding
private lateinit var mainViewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainActivityBinding = DataBindingUtil.setContentView(
this,
R.layout.activity_main
)
mainActivityBinding.rvmainRepos.adapter = mainAdapter
AndroidInjection.inject(this)
mainViewModel =
ViewModelProviders.of(
this#MainActivity,
mainViewModelFactory
)[mainViewModel::class.java]
mainActivityBinding.viewmodel = mainViewModel
mainActivityBinding.lifecycleOwner = this
mainViewModel.mainRepoReponse.observe(this, Observer<Response> {
repoList.clear()
it.success?.let { response ->
if (!response.isEmpty()) {
// mainViewModel.saveDataToDb(response)
// mainViewModel.createWorkerForClearingDb()
}
}
})
}
}
MainViewModelFactory
class MainViewModelFactory #Inject constructor(
val mainRepository: mainRepository
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>) =
with(modelClass) {
when {
isAssignableFrom(mainViewModel::class.java) -> mainViewModel(
mainRepository = mainRepository
)
else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
}
} as T
}
MainViewModel
class MainViewModel(
val mainRepository: mainRepository
) : ViewModel() {
private val compositeDisposable = CompositeDisposable()
val mainRepoReponse = MutableLiveData<Response>()
val loadingProgress: MutableLiveData<Boolean> = MutableLiveData()
val _loadingProgress: LiveData<Boolean> = loadingProgress
val loadingFailed: MutableLiveData<Boolean> = MutableLiveData()
val _loadingFailed: LiveData<Boolean> = loadingFailed
var isConnected: Boolean = false
fun fetchmainRepos() {
if (isConnected) {
loadingProgress.value = true
compositeDisposable.add(
mainRepository.getmainRepos().subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ response ->
run {
saveDataToDb(response)
)
}
},
{ error ->
processResponse(Response(AppConstants.Status.SUCCESS, null, error))
}
)
)
} else {
fetchFromLocal()
}
}
private fun saveDataToDb(response: List<mainRepo>) {
mainRepository.insertmainUsers(response)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.subscribe(object : DisposableCompletableObserver() {
override fun onComplete() {
Log.d("Status", "Save Success")
}
override fun onError(e: Throwable) {
Log.d("Status", "error ${e.localizedMessage}")
}
})
}
}
MainRepository
interface MainRepository {
fun getmainRepos(): Single<List<mainRepo>>
fun getAllLocalRecords(): Single<List<mainRepo>>
fun insertmainUsers(repoList: List<mainRepo>): Completable
}
MainRepositoryImpl
class mainRepositoryImpl #Inject constructor(
val apiService: GitHubApi,
val mainDao: AppDao
) : MainRepository {
override fun getAllLocalRecords(): Single<List<mainRepo>> = mainDao.getAllRepos()
override fun insertmainUsers(repoList: List<mainRepo>) :Completable{
return mainDao.insertAllRepos(repoList)
}
override fun getmainRepos(): Single<List<mainRepo>> {
return apiService.getmainGits()
}
}
I'm quite confused with the implementation of MVVM with LiveData and Rxjava, in my MainViewModel I am calling the interface method and implementing it inside ViewModel, also on the response I'm saving the response to db. However, that is a private method, which won't be testable in unit testing in a proper way (because it's private). What is the best practice to call other methods on the completion of one method or i have to implement all the methods inside the implementation class which uses the interface.
Your ViewModel should not care how you are getting the data if you are trying to follow the clean architecture pattern. The logic for fetching the data from local or remote sources should be in the repository in the worst case where you can also save the response. In that case, since you have a contact for the methods, you can easily test them. Ideally, you could break it down even more - adding Usecases/Interactors.

Write unit Testcase for ViewModel in kotlin

I am using Junit & Mockito 4 for unit testing of viewModel.
ViewModel class
class MainViewModel(app: Application, private val githubRepo: GithubRepository) :
BaseViewModel(app) {
private val _trendingLiveData by lazy { MutableLiveData<Event<DataState<List<TrendingResponse>>>>() }
val trendingLiveData: LiveData<Event<DataState<List<TrendingResponse>>>> by lazy { _trendingLiveData }
var loadingState = MutableLiveData<Boolean>()
fun getTrendingData(language: String?, since: String?) {
launch {
loadingState.postValue(true)
when (val result = githubRepo.getTrendingListAsync(language, since).awaitAndGet()) {
is Result.Success -> {
loadingState.postValue(false)
result.body?.let {
Event(DataState.Success(it))
}.run(_trendingLiveData::postValue)
}
is Result.Failure -> {
loadingState.postValue(false)
}
}
}
}
}
Api EndPoinit
interface GithubRepository {
fun getTrendingListAsync(
language: String?,
since: String?
): Deferred<Response<List<TrendingResponse>>>
}
ViewModel Test class
#RunWith(JUnit4::class)
class MainViewModelTest {
#Rule
#JvmField
val instantTaskExecutorRule = InstantTaskExecutorRule()
#Mock
lateinit var repo: GithubRepository
#Mock
lateinit var githubApi: GithubApi
#Mock
lateinit var application: TrendingApp
lateinit var viewModel: MainViewModel
#Mock
lateinit var dataObserver: Observer<Event<DataState<List<TrendingResponse>>>>
#Mock
lateinit var loadingObserver: Observer<Boolean>
private val threadContext = newSingleThreadContext("UI thread")
private val trendingList : List<TrendingResponse> = listOf()
#Before
fun setUp() {
MockitoAnnotations.initMocks(this)
Dispatchers.setMain(threadContext)
viewModel = MainViewModel(application, repo)
}
#Test
fun test_TrendingRepo_whenSuccess() {
//Assemble
Mockito.`when`(githubApi.getTrendingListAsync("java", "daily"))
.thenAnswer{ return#thenAnswer trendingList.toDeferred() }
//Act
viewModel.trendingLiveData.observeForever(dataObserver)
viewModel.loadingState.observeForever(loadingObserver)
viewModel.getTrendingData("java", "daily")
Thread.sleep(1000)
//Verify
verify(loadingObserver).onChanged(true)
//verify(dataObserver).onChanged(trendingList)
verify(loadingObserver).onChanged(false)
}
#After
fun tearDown() {
Dispatchers.resetMain()
threadContext.close()
}
}
Problem is that my livedata is wrapped around Event<DataState<List<TrendingResponse>>, due to which I am not able to get what should be dataObserver and how should I verify that dataObserver in the test class.
Event os open class that is to handle event like SingleLiveData
DataState is sealed class that contain SUCCESS & FAILED data class
I have written test case livedata is like LiveData<List<Response> or something like that.
You need to wrap the List<TrendingResponse> → Event(DataState.Success(List<TrendingResponse>)) which you are returning using mockito - trendingList.toDeferred().
#Test
fun test_TrendingRepo_whenSuccess() {
//Assemble
Mockito.`when`(githubApi.getTrendingListAsync("java", "daily"))
.thenAnswer{ return#thenAnswer trendingList.toDeferred() }
//Act
viewModel.trendingLiveData.observeForever(dataObserver)
viewModel.loadingState.observeForever(loadingObserver)
viewModel.getTrendingData("java", "daily")
Thread.sleep(1000)
//Verify
verify(loadingObserver).onChanged(true)
//wrap the trendingList inside Event(DataState(YourList))
verify(dataObserver).onChanged(Event(DataState.Success(trendingList)))
verify(loadingObserver).onChanged(false)
}

Categories

Resources