Write unit Testcase for ViewModel in kotlin - android

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

Related

Disable/cancel StateFlow for unit test

In my ViewModel there's a StateFlow which seems to prevent my unit test from ever completing, i.e. the test "hangs". I'm fairly new to the Flow lib and not sure how to cancel/disable said StateFlow so I can run my test as normal.
I created a simplified version of the code to highlight my problem. Here's the ViewModel in question:
#ExperimentalCoroutinesApi
class MyViewModel(
private val someApiClient: SomeApiClient,
private val dispatchers: CoroutineContexts = DefaultCoroutineContexts,
private val someLogger: SomeLogger
) : ViewModel() {
private val queries = MutableSharedFlow<String>()
#FlowPreview
val suggestions: StateFlow<Result<Throwable, String>> =
queries
.sample(THROTTLE_TIME)
.distinctUntilChanged()
.mapLatest {
try {
val downloadedSuggestions = someApiClient.getSuggestions(it)
Result.Success(downloadedSuggestions)
} catch (exception: Throwable) {
Result.Error(exception)
}
}
.flowOn(dispatchers.io)
.stateIn(viewModelScope, SharingStarted.Eagerly, Result.Success(""))
fun dispatchEvent(event: MyEvent) {
when (event) {
is SearchEvent -> someLogger.logStuff()
}
}
}
And the test looks like this:
#ExperimentalCoroutinesApi
class MyViewModelTest {
#get:Rule var mockitoRule: MockitoRule = MockitoJUnit.rule()
#get:Rule val coroutinesTestRule = MainDispatcherRule()
#Mock lateinit var someApiClient: SomeApiClient
#Mock lateinit var someLogger: SomeLogger
lateinit var myViewModel: MyViewModel
#Before
fun setUp() {
myViewModel = MyViewModel(
someApiClient,
coroutinesTestRule.testDispatcher.createCoroutineContexts(),
someLogger
)
}
#Test
fun `my test`() = runTest {
// When
myViewModel.dispatchEvent(SearchEvent)
// Then
verify(someLogger).logStuff()
}
}
I tried a suggestion from the Android Developer documentation page in the test:
#Test
fun `my test`() = runTest {
// Given
val dispatcher = UnconfinedTestDispatcher(testScheduler)
val job = launch(dispatcher) { myViewModel.suggestions.collect() }
// When
myViewModel.dispatchEvent(SearchEvent)
// Then
verify(someLogger).logStuff()
job.cancel()
}
But it didn't help. I feel like I'm missing something fairly obvious but can't quite put my finger on it. Any suggestions are welcome.

Android unit testing using Mockito : can't get the right behaviour for mocks

I am testing my Repository class using Mockito, specifically getProducts() functionality:
class Repository private constructor(private val retrofitService: ApiService) {
companion object {
#Volatile
private var INSTANCE: Repository? = null
fun getInstance(retrofitService: ApiService): Repository {
synchronized(this) {
var instance = INSTANCE
if (instance == null) {
instance = Repository(retrofitService)
}
INSTANCE = instance
return instance
}
}
}
suspend fun getProducts(): ProductsResponse = withContext(IO) {
retrofitService.getProducts()
}
}
This is my test class:
#ExperimentalCoroutinesApi
#RunWith(MockitoJUnitRunner::class)
class RepositoryTest {
// Class under test
private lateinit var repository: Repository
// Executes each task synchronously using Architecture Components.
#get:Rule
val instantExecutorRule = InstantTaskExecutorRule()
// Set the main coroutines dispatcher for unit testing.
#ExperimentalCoroutinesApi
#get:Rule
var mainCoroutineRule = MainCoroutineRule()
#Mock
private lateinit var retrofitService: ApiService
#Before
fun createRepository() {
MockitoAnnotations.initMocks(this)
repository = Repository.getInstance(retrofitService)
}
#Test
fun test() = runBlocking {
// GIVEN
Mockito.`when`(retrofitService.getProducts()).thenReturn(fakeProductsResponse)
// WHEN
val productResponse: ProductsResponse = repository.getProducts()
println("HERE = ${retrofitService.getProducts()}")
// THEN
println("HERE: $productResponse")
MatcherAssert.assertThat(productResponse, `is`(fakeProductsResponse))
}
}
And my ApiService:
interface ApiService {
#GET("https://www...")
suspend fun getProducts(): ProductsResponse
}
When I call repository.getProducts(), it returns null despite the fact, that I explicitly set retrofitService.getProducts() to return fakeProductsResponse, which is being called inside repository's getProducts() method. It should return fakeProductsResponse, but it returns null.
Am I doing wrong mocking or what the problem can be? Thanks...
EDIT: this is my MainCoroutineRule, if you need it
#ExperimentalCoroutinesApi
class MainCoroutineRule(val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()):
TestWatcher(),
TestCoroutineScope by TestCoroutineScope(dispatcher) {
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
cleanupTestCoroutines()
Dispatchers.resetMain()
}
}
It might not be a complete solution to your problem, but what I see is that your MainCoroutineRule overrides the mainDispatcher Dispatchers.setMain(dispatcher).
But in
suspend fun getProducts(): ProductsResponse = withContext(IO)
you are explicitely setting an IO Dispatcher.
I recommend always setting the dispatcher from a property you pass via constructor:
class Repository private constructor(
private val retrofitService: ApiService,
private val dispatcher: CoroutineDispatcher) {
companion object {
fun getInstance(retrofitService: ApiService,
dispatcher: CoroutineDispatcher = Dispatchers.IO): Repository {
// ommit code for simplicity
instance = Repository(retrofitService, dispatcher)
// ...
}
}
}
suspend fun getProducts(): ProductsResponse = withContext(dispatcher) {
retrofitService.getProducts()
}
}
Having it a default parameter you do not need to pass it in your regular code, but you can exchange it within your unit test:
class RepositoryTest {
private lateinit var repository: Repository
#get:Rule
var mainCoroutineRule = MainCoroutineRule()
#Mock
private lateinit var retrofitService: ApiService
#Before
fun createRepository() {
MockitoAnnotations.initMocks(this)
repository = Repository.getInstance(retrofitService, mainCoroutineRule.dispatcher)
}
}
For my own unit tests I am using the blocking function of TestCoroutineDispatcher from the CoroutineRule like:
#Test
fun aTest() = mainCoroutineRule.dispatcher.runBlockingTest {
val acutal = classUnderTest.callToSuspendFunction()
// do assertions
}
I hope this will help you a bit.

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.

Test LiveData and Coroutines using MockK

I have this view model:
class MyViewModel(private val myUseCase: MyUseCase) : ViewModel() {
val stateLiveData = MutableLiveData(State.IDLE)
fun onButtonPressed() {
viewModelScope.launch {
stateLiveData.value = State.LOADING
myUseCase.loadStuff() // Suspend
stateLiveData.value = State.SUCCESS
}
}
}
I'd like to write a test that checks whether the state is really LOADING while myUseCase.loadStuff() is running. I'm using MockK for that. Here's the test class:
#ExperimentalCoroutinesApi
class MyViewModelTest {
#get:Rule
val rule = InstantTaskExecutorRule()
private lateinit var myUseCase: MyUseCase
private lateinit var myViewModel: MyViewModel
#Before
fun setup() {
myUseCase = mockkClass(MyUseCase::class)
myViewModel = MyViewModel(myUseCase)
}
#Test
fun `button click should put screen into loading state`() = runBlockingTest {
coEvery { myUseCase.loadStuff() } coAnswers { delay(2000) }
myViewModel.onButtonPressed()
advanceTimeBy(1000)
val state = myViewModel.stateLiveData.value
assertEquals(State.LOADING, state)
}
}
It fails:
java.lang.AssertionError:
Expected :LOADING
Actual :IDLE
How can I fix this?
I only needed to make a few changes in the test class to make it pass:
#ExperimentalCoroutinesApi
class MyViewModelTest {
#get:Rule
val rule = InstantTaskExecutorRule()
private val dispatcher = TestCoroutineDispatcher()
private lateinit var myUseCase: MyUseCase
private lateinit var myViewModel: MyViewModel
#Before
fun setup() {
Dispatchers.setMain(dispatcher)
myUseCase = mockkClass(MyUseCase::class)
myViewModel = MyViewModel(myUseCase)
}
#After
fun cleanup() {
Dispatchers.resetMain()
}
#Test
fun `button click should put screen into loading state`() {
dispatcher.runBlockingTest {
coEvery { myUseCase.loadStuff() } coAnswers { delay(2000) }
myViewModel.onButtonPressed()
// This isn't even needed.
advanceTimeBy(1000)
val state = myViewModel.stateLiveData.value
assertEquals(State.LOADING, state)
}
}
}
No changes needed in the view model at all! :D
Thanks Kiskae for such helpful advice!
Your problem lies in the fact that viewModelScope dispatches to Dispatcher.MAIN, not the testing dispatcher created by runBlockingTest. This means that even with the call to advanceTimeBy the code does not get executed.
You can solve the issue by using Dispatcher.setMain(..) to replace the MAIN dispatcher with your test dispatcher. This will require managing the dispatcher yourself instead of relying on the stand-alone runBlockingTest.

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