Unit Test : Observer onChanged should be called twice instead of once - android

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

Related

How do I invoke a callback in a unit test in Android?

I want to invoke a callback to assert the execution it makes.
I'm using MVVM in my app. In one of the view models I implemented, I want to make sure the ui state changes when a process is completed.
In my HomeViewModel.kt I have:
#HiltViewModel
class HomeViewModel
#Inject
constructor(
private val storageRepository: StorageRepository,
private val accountRepository: AccountRepository,
) : ViewModel() {
// First state of isLoading is true
var uiState = mutableStateOf(HomeUiState())
...
fun addListener() {
viewModelScope.launch {
storageRepository.addListener(
accountRepository.getUserId(),
::onDocumentEvent,
onComplete = {
uiState.value = uiState.value.copy(isLoading = false)
},
onError = {
error -> onAddListenerFailure(error)
}
)
}
}
And I want to write the test:
Given homeViewModel.addListener()
When storageRepository.addListener(...) completes
Then uiState.isLoading is false
I've been searching for some time now and I have found some people referring to using captors from mockito but nothing that applies to my case.
This is what I have now
#OptIn(ExperimentalCoroutinesApi::class)
internal class HomeViewModelTest {
// mock repositories
#Mock lateinit var storageRepository: StorageRepository
#Mock lateinit var accountRepository: AccountRepository
#Mock lateinit var logRepository: LogRepository
// set dispatcher to be able to run tests
private val dispatcher = StandardTestDispatcher()
lateinit var callbackCaptor: KArgumentCaptor<() -> Unit>
#Before
fun setUp() {
MockitoAnnotations.openMocks(this)
Dispatchers.setMain(dispatcher)
}
#After
fun tearDown() {
Dispatchers.resetMain()
}
#Test
fun `loading state is true when viewModel is created`() {
val homeViewModel = HomeViewModel(storageRepository, accountRepository, logRepository)
assertTrue(homeViewModel.uiState.value.isLoading)
}
#Test
fun `loading state is false when listener is added successfully`() {
val homeViewModel = HomeViewModel(storageRepository, accountRepository, logRepository)
callbackCaptor = argumentCaptor()
whenever(
storageRepository.addListener(
anyString(),
anyOrNull(),
callbackCaptor.capture(),
anyOrNull()
)
)
.thenAnswer { callbackCaptor.firstValue.invoke() }
homeViewModel.addListener()
// wait for mutable state to update
dispatcher.scheduler.advanceUntilIdle()
assertFalse(homeViewModel.uiState.value.isLoading)
}
}
Of course, I'm open to hearing solutions using something else than captors.
I think you are not initialising the captor, try following
#Test
fun `loading state is false when listener completes its process`() {
val homeViewModel = HomeViewModel(storageRepository, accountRepository, logRepository)
val callbackCaptor = argumentCaptor<() -> Unit>() //used kotlin mockito
whenever(storageRepository.addListener(anyString(), any(), callbackCaptor.capture(), any()))
.thenAnswer { callbackCaptor.firstValue.invoke() }
homeViewModel.addListener()
// wait for mutable state to update
dispatcher.scheduler.advanceUntilIdle()
assertFalse(homeViewModel.uiState.value.isLoading)
}

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.

Mocked multiple times, but mocked value does not change after secondly mocked in different test method

I'm writing unit test for my ViewModel. I have mocked my data source and want to test that datasource returns success and error cases. If i run tests individually everything is OK.
In the first method i mocked to return success, in the second method i mocked to return error. When i run these 2 tests together (by clicking run tests in class name), in the second method i want dataSource.getPackageCard() to return ResponseState.Error("error1337") however it returns ResponseState.Success(responseDto). In other words, it remembers the mocked value from 1st method. Why ? How to solve that problem ?
#MediumTest
#RunWith(AndroidJUnit4::class)
#ExperimentalCoroutinesApi
class MyViewModelTest {
#get: Rule
var instantExecutorRule = InstantTaskExecutorRule()
#get: Rule
var mainCoroutineRule = MainCoroutineRule()
private lateinit var viewModel: MyViewModel
lateinit var MyRepository: MyRepository
val responseDto = MyResponseDto().apply {
val myList = mutableListOf<CardListGroupDTO>()
myList.add(CardListGroupDTO(cardGroupType = "test",
headerTitle = "test",
buttonAll = ButtonDto(title = "test", url = "test")
))
groupList = myList
}
#MockK
lateinit var dataSource: MyDataSource
#Before
fun setup() {
MockKAnnotations.init(this)
MyRepository = MyRepositoryImpl.getInstance(dataSource)
viewModel = MyViewModel(MyRepository)
}
#After
fun afterTests() {
unmockkAll()
unmockkObject(dataSource)
}
#Test
fun `test successful case`() = runBlockingTest {
// given
coEvery {
dataSource.getPackageCard()
} returns ResponseState.Success(
responseDto
)
var counter = 0
viewModel.MyResponseDto.observeForever(
object : Observer<ResponseState<MyResponseDto>> {
override fun onChanged(t: ResponseState<MyResponseDto>) {
// println(viewModel.MyResponseDto.value)
when (counter) {
0 ->
Truth.assertThat(t).isEqualTo(ResponseState.Loading(true))
1 ->
Truth.assertThat(t).isEqualTo(ResponseState.Success(responseDto))
2 -> {
Truth.assertThat(t).isEqualTo(ResponseState.Loading(false))
viewModel.MyResponseDto.removeObserver(this)
}
}
counter++
}
})
viewModel.getPackageCard()
}
#Test
fun `test error case`() = runBlockingTest {
val errorMessage = "error1337"
// given
coEvery {
dataSource.getPackageCard()
} returns ResponseState.Error(
errorMessage
)
var counter = 0
viewModel.MyResponseDto.observeForever(
object : Observer<ResponseState<MyResponseDto>> {
override fun onChanged(t: ResponseState<MyResponseDto>) {
// println(viewModel.MyResponseDto.value)
when (counter) {
0 ->
Truth.assertThat(t).isEqualTo(ResponseState.Loading(true))
1 ->
Truth.assertThat(t).isEqualTo(ResponseState.Error(errorMessage))
2 -> {
Truth.assertThat(t).isEqualTo(ResponseState.Loading(false))
viewModel.MyResponseDto.removeObserver(this)
}
}
counter++
}
})
viewModel.getPackageCard()
}
}
I found the answer finally. Since i use static repository MyRepositoryImpl.getInstance(dataSource), the datasource is mocked once. Second mock is not valid. I did manual singleton, inside companioan object create if it is not null, if nonnull return the object. This is the cause of my problem.
I solved the problem, by removing the singleton pattern i implemented as the above. I used constructor injection and made my repository singleton in this way. In my unit tests my repository is not singleton any more.
#Singleton
class MyRepositoryImpl #Inject constructor(
private val myRemoteDataSource: MyRemoteDataSource
) : MyRepository
And my viewmodel test is fixed when i write the following :
#Before
fun setup() {
MockKAnnotations.init(this)
myRepository = MyRepositoryImpl(dataSource)
viewModel = MyViewModel(myRepository)
}

How to make viewModelScope wait for a suspend function in unit test

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

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.

Categories

Resources