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.
Related
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.
As we know the default dispatcher for viewModelScope executes in parallel. How do I make viewModelScope wait for the suspend function in unit test . In my code i would like to get the result from repository.getData() so that my assertion passes. Right now, i don't get the result on time.
class MainViewModel(private val repository: Repository,
private val dispatcher: CoroutineDispatcherProvider) : ViewModel() {
private val viewState = MutableLiveData<Results<Data>>()
fun getViewState() : LiveData<Results<Data>> = viewState
fun getData(query: search) {
viewState.value = Loading
viewModelScope.launch(dispatcher.main()) {
val results = repository.getData(query) //need to wait for this
when(results){
is Success -> {
viewState.value = Success(results.data)
}
is Error -> viewState.value = Error(“Error”)
}
}
MainViewModelTest:
Class MainViewModelTest {
#get:Rule
val instantExecutorRule = InstantTaskExecutorRule()
#get:Rule
val coroutineTestRule = CoroutineTestRule()
#Test
fun `test get data`() = coroutineTestRule.testCoroutineDispatcher.runBlockingTest {
coEvery{repository.getData(“query”)} returns Success(Data)
var observer:Observer<Results<Data>> = mock()
viewModel.getViewState().observeForever(observer)
viewModel.getData(“query”)
assertEquals(Loading, state)
assertEquals(resultSuccess, state)
}
}
I think you should use observer.onChanged() for assertions and verifications. I don't see a problem otherwise.
val successResult = Result.Success(Data)
coEvery{repository.getData(“query”)} returns successResult
verify { observer.onChanged(Result.Loading) }
verify { observer.onChanged(successResult) }
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)
}
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.
Why do I get different results when unit testing my ViewModel?
I got two tests. When I launch each test individually that's ok but when I launch all tests in a row I got an error.
It's a ViewModel that change state each time I got a return from an
API. I expect to get android.arch.lifecycle.Observer.onChanged called two times but it's just called once for the second test.
Unit test works fine when I replace verify(view, times(2)).onChanged(arg.capture()) with verify(view, atLeastOnce()).onChanged(arg.capture()) at the first test.
UserViewModel :
class UserViewModel(
private val leApi: LeApi
): ViewModel() {
private val _states = MutableLiveData<ViewModelState>()
val states: LiveData<ViewModelState>
get() = _states
fun getCurrentUser() {
_states.value = LoadingState
leApi.getCurrentUser()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ user -> _states.value = UserConnected(user) },
{ t -> _states.value = FailedState(t) }
)
}
}
}
UserViewModelTest :
#RunWith(MockitoJUnitRunner::class)
class UserViewModelTest {
lateinit var userViewModel: UserViewModel
#Mock
lateinit var view: Observer<ViewModelState>
#Mock
lateinit var leApi: LeApi
#get:Rule
val rule = InstantTaskExecutorRule()
#Before
fun setUp() {
RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
userViewModel = UserViewModel(leApi)
userViewModel.states.observeForever(view)
}
#Test
fun testGetCurrentUser() {
val user = Mockito.mock(User::class.java)
`when`(leApi.getCurrentUser()).thenReturn(Single.just(user))
userViewModel.getCurrentUser()
val arg = ArgumentCaptor.forClass(ViewModelState::class.java)
verify(view, times(2)).onChanged(arg.capture())
val values = arg.allValues
assertEquals(2, values.size)
assertEquals(LoadingState, values[0])
assertEquals(UserConnected(user), values[1])
}
#Test
fun testGetCurrentUserFailed() {
val error = Throwable("Got error")
`when`(leApi.getCurrentUser()).thenReturn(Single.error(error))
userViewModel.getCurrentUser()
val arg = ArgumentCaptor.forClass(ViewModelState::class.java)
verify(view, times(2)).onChanged(arg.capture()) // Error occurred here. That's the 70th line from stack trace.
val values = arg.allValues
assertEquals(2, values.size)
assertEquals(LoadingState, values[0])
assertEquals(FailedState(error), values[1])
}
}
Expected :
All tests passed.
Actual :
org.mockito.exceptions.verification.TooLittleActualInvocations:
view.onChanged(<Capturing argument>);
Wanted 2 times:
-> at com.dev.titi.toto.mvvm.UserViewModelTest.testGetCurrentUserFailed(UserViewModelTest.kt:70)
But was 1 time:
-> at android.arch.lifecycle.LiveData.considerNotify(LiveData.java:109)
I had this exact problem. I changed the way of testing to following (Google recommendations, here are the classes used for following test):
Add coroutines to your project, since test helpers use them:
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1")
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.0'
Get rid of this line:
lateinit var view: Observer<ViewModelState>
Then change your test to following:
private val testDispatcher = TestCoroutineDispatcher()
#Before
fun setup() {
Dispatchers.setMain(testDispatcher)
...
}
#After
fun tearDown() {
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
...
}
#Test
fun testGetCurrentUser() {
runBlocking {
val user = Mockito.mock(User::class.java)
`when`(leApi.getCurrentUser()).thenReturn(Single.just(user))
userViewModel.states.captureValues {
userViewModel.getCurrentUser()
assertSendsValues(100, LoadingState, UserConnected(user))
}
}
}