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)
}
Related
I'm trying to unit test my ViewModel. It has a state which is of the type StateFlow.
When I call my useCase, I set the value of the state to Loading in the onStart block as follows:
#HiltViewModel
class MyViewModel #Inject constructor(private val myUseCase: MyUseCase) : ViewModel() {
// ...
private val _state: MutableStateFlow<MyState> = MutableStateFlow(MyState.Loading)
val state = _state.asStateFlow()
init {
loadData()
}
fun loadData() {
viewModelScope.launch {
myUseCase().onStart {
_state.value = MyState.Loading
}.collect { data ->
// ...
_state.value = MyState.Data(data)
}
}
}
// ...
}
I'm trying to test that the first emitted value from the flow is the Loading state but my test fails and says expected Data not Loading.
Here is my test class:
class MyViewModelTest {
private lateinit var viewModel: MyViewModel
private lateinit var fakeUseCase: MyFakeUseCase
#BeforeEach
fun setup() {
fakeUseCase = MyFakeUseCase()
viewModel = MyViewModel(fakeUseCase)
}
#Test
fun loadData_anyCriteria_stateIsLoadingOnStart() {
runBlocking {
// Act
viewModel.loadData()
val result = viewModel.state.first()
// Assert
Assertions.assertEquals(result, MyState.Loading)
}
}
}
Test result:
Expected :Data(bla bla bla)
Actual :com.example....MyState$Loading#7b8233cd
How can I test the first value emitted from a StateFlow?
Consider the following code:
sealed interface State {
object Loading : State
data class Content(val someString: String) : State
}
class SomeViewModel(getSomeStringUseCase: GetSomeStringUseCase) : ViewModel() {
private val _someString = MutableLiveData<State>()
val someString: LiveData<State> = _someString
init {
_someString.value = State.Loading
viewModelScope.launch {
_someString.value = State.Content(getSomeStringUseCase())
}
}
}
In a unit test it's pretty simple to test and assert the last value emitted by someString, however, if I want to assert all values emitted it gets more complicated because I can't subscribe to someString before SomeViewModel is initialized and if I do the subscription right after the initialization it is too late and the values were already emitted:
class SomeViewModelTest {
#MockK
private lateinit var getSomeStringUseCase: GetSomeStringUseCase
private lateinit var viewModel: SomeViewModel
#Before
fun setUp() {
coEvery { getSomeStringUseCase() } returns "Some String!"
viewModel = SomeViewModel(getSomeStringUseCase)
}
// This test fails
#Test
fun test() {
val observer = mockk<Observer<State>>(relaxed = true)
viewModel.someString.observeForever(observer)
verifyOrder {
observer.onChanged(State.Loading)
observer.onChanged(State.Content("Some String!"))
}
}
}
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)
}
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))
}
}
}