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?
Related
I want to write a simple test for my viewModel to check if it gets data from repository. The app itself working without problem but in test, i have the following test failed.
It looks like the viewModel init block not running, because it suppose to call getUpcomingMovies() method in init blocks and post value to upcomingMovies live data object. When i test it gets null value.
Looks like i am missing a minor thing, need help to solve this.
Here is the test:
#ExperimentalCoroutinesApi
class MoviesViewModelShould: BaseUnitTest() {
private val repository: MoviesRepository = mock()
private val upcomingMovies = mock<Response<UpcomingResponse>>()
private val upcomingMoviesExpected = Result.success(upcomingMovies)
#Test
fun emitsUpcomingMoviesFromRepository() = runBlocking {
val viewModel = mockSuccessfulCaseUpcomingMovies()
assertEquals(upcomingMoviesExpected, viewModel.upcomingMovies.getValueForTest())
}
private fun mockSuccessfulCaseUpcomingMovies(): MoviesViewModel {
runBlocking {
whenever(repository.getUpcomingMovies(1)).thenReturn(
flow {
emit(upcomingMoviesExpected)
}
)
}
return MoviesViewModel(repository)
}
}
And viewModel:
class MoviesViewModel(
private val repository: MoviesRepository
): ViewModel() {
val upcomingMovies: MutableLiveData<UpcomingResponse> = MutableLiveData()
var upcomingMoviesPage = 0
private var upcomingMoviesResponse: UpcomingResponse? = null
init {
getUpcomingMovies()
}
fun getUpcomingMovies() = viewModelScope.launch {
upcomingMoviesPage++
repository.getUpcomingMovies(upcomingMoviesPage).collect { result ->
if (result.isSuccess) {
result.getOrNull()!!.body()?.let {
if (upcomingMoviesResponse == null) {
upcomingMoviesResponse = it
} else {
val oldMovies = upcomingMoviesResponse?.results
val newMovies = it.results
oldMovies?.addAll(newMovies)
}
upcomingMovies.postValue(upcomingMoviesResponse ?: it)
}
}
}
}
}
And the result is:
expected:<Success(Mock for Response, hashCode: 1625939772)> but was:<null>
Expected :Success(Mock for Response, hashCode: 1625939772)
Actual :null
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)
}
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 am using espresso for testing and I am getting an error while executing below method.
viewModel.getData().observe(viewLifecycleOwner, Observer { options ->
Log.d(TAG, "onViewCreated: $options")
val data = options.filter { option -> option.type!! == OptionType.DATA}
updateData.updateUIComponent(data)
})
get data method return a LiveData object. and this is working fine in the Fragments but not workng in the espresso test class
Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
This is my test class
#LargeTest
#RunWith(AndroidJUnit4::class)
open class BaseIntegrationTest {
#get:Rule
val instantTestExecutorRule = InstantTaskExecutorRule()
lateinit var navController: TestNavHostController
lateinit var fragmentScenario: FragmentScenario<DetailsFragment>
#Before
fun initTest() {
navController = TestNavHostController(
ApplicationProvider.getApplicationContext()
)
}
#Test
fun launchFragment() {
val bundle = Bundle()
fragmentScenario =
launchFragmentInContainer(bundle, themeResId = R.style.Theme_mainTheme)
fragmentScenario.moveToState(Lifecycle.State.STARTED)
UiThreadStatement.runOnUiThread {
navController.setGraph(R.navigation.app_navigation_graph)
}
fragmentScenario.onFragment { fragment ->
//CollectFeedbackFragment()
Navigation.setViewNavController(fragment.requireView(), navController)
}
onView(withId(R.id.progressBar)).isVisible()
}
}
I managed to fix this issue By using Dispatchers.IO and Dispatchers.Main
class SampleViewModel : ViewModel() {
fun getData():DataLiveData {
viewModelScope.launch {
withContext(Dispatchers.IO) {
// here I am seding the Room db call
withContext(Dispatchers.Main) {
// I am updating the MutableLiveData object here
}
}
}
return mutableLiveData
}
}
And don't forget to inject dispatchers instead of hardcoding.
Click here!
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)
}