how can I apply mockK when viewModel uses pref in init block? - android

Here's my ViewModel class.
#HiltViewModel
class MainViewModel #Inject constructor(
private val serviceRepository: ServiceRepository,
private val userPrefRepository: UserPreferencesRepository,
override val preferencesRepository: PreferencesRepository,
) : BaseViewModel() {
private val _menuList = mutableResultState<MenuData>(ResultState.UnInitialize)
val menuListState: StateFlow<ResultState<MenuData>>
get() = _menuList.asStateFlow()
var isCancelButtonVisible: Boolean = false
init {
isCancelButtonVisible = getCancelButtonVisibility()
}
// I used this to test private method.
#VisibleForTesting(otherwise = PRIVATE)
fun getCancelButtonVisibility(): Boolean = preferencesRepository.cancelButtonVisibility
fun fetchMenus() = viewModelScope.launch{
serviceRepository.fetchMenus()
.onState {
_menuList.value = it
}
}
}
FYI, I never have written test codes. And at the moment, I'd like to test fetchMenus() method. So, maybe I need to check menuListState is set after calling fetchMenus().
(I will use turbine library to check flow data)
// TODO: must initialize getCancelButtonVisibility first and then need to set something to test MainViewModel and fetchMenus() method.
#Test
fun fetchMenus_success_returnMenuList() = runTest {
// given
coEvery { mainViewModel.getCancelButtonVisibility()} returns true
// then
mainViewModel.menuListState.test {
assertNotNull(awaitItem())
}
}
I get error this error after running the test:
no answer found for: PreferencesRepository(#2).getCancelButtonVisibility()
io.mockk.MockKException: no answer found for: PreferencesRepository(#2).getCancelButtonVisibility()

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

Unit testing viewModel that uses StateFlow and Coroutines

Kotlin 1.4.21
I have a very simple ViewModel that uses coroutine and stateFlow. However, the unit test will fail as the stateFlow doesn't seem to get updated.
I think its because the test will finish before the stateFlow is updated.
expected not to be empty
This is my ViewModel under test
class TrendingSearchViewModel #Inject constructor(
private val loadTrendingSearchUseCase: LoadTrendingSearchUseCase,
private val coroutineDispatcher: CoroutineDispatcherProvider
) : ViewModel() {
private val trendingSearchMutableStateFlow = MutableStateFlow<List<String>>(emptyList())
val trendingSearchStateFlow = trendingSearchMutableStateFlow.asStateFlow()
fun getTrendingSearch() {
viewModelScope.launch(coroutineDispatcher.io()) {
try {
trendingSearchMutableStateFlow.value = loadTrendingSearchUseCase.execute()
} catch (exception: Exception) {
Timber.e(exception, "trending ${exception.localizedMessage}")
}
}
}
}
This is my actual test class, I have tried different things to get it to work
class TrendingSearchViewModelTest {
private val loadTrendingSearchUseCase: LoadTrendingSearchUseCase = mock()
private val coroutineDispatcherProvider = CoroutineDispatcherProviderImp()
private lateinit var trendingSearchViewModel: TrendingSearchViewModel
#Before
fun setUp() {
trendingSearchViewModel = TrendingSearchViewModel(
loadTrendingSearchUseCase,
coroutineDispatcherProvider
)
}
#Test
fun `should get trending search suggestions`() {
runBlocking {
// Arrange
val trending1 = UUID.randomUUID().toString()
val trending2 = UUID.randomUUID().toString()
val trending3 = UUID.randomUUID().toString()
whenever(loadTrendingSearchUseCase.execute()).thenReturn(listOf(trending1, trending2, trending3))
val job = launch {
trendingSearchViewModel.trendingSearchStateFlow.value
}
// Act
trendingSearchViewModel.getTrendingSearch()
// Assert
val result = trendingSearchViewModel.trendingSearchStateFlow.value
assertThat(result).isNotEmpty()
job.cancel()
}
}
}
This is the usecase I am mocking in the test:
class LoadTrendingSearchUseCaseImp #Inject constructor(
private val searchCriteriaProvider: SearchCriteriaProvider,
private val coroutineDispatcherProvider: CoroutineDispatcherProvider
) : LoadTrendingSearchUseCase {
override suspend fun execute(): List<String> {
return withContext(coroutineDispatcherProvider.io()) {
searchCriteriaProvider.provideTrendingSearch().trendingSearches
}
}
}
Just in case its needed this is my interface:
interface CoroutineDispatcherProvider {
fun io(): CoroutineDispatcher = Dispatchers.IO
fun default(): CoroutineDispatcher = Dispatchers.Default
fun main(): CoroutineDispatcher = Dispatchers.Main
fun immediate(): CoroutineDispatcher = Dispatchers.Main.immediate
fun unconfined(): CoroutineDispatcher = Dispatchers.Unconfined
}
class CoroutineDispatcherProviderImp #Inject constructor() : CoroutineDispatcherProvider
I think this library https://github.com/cashapp/turbine by Jack Wharton will be of great help in the future when you need more complex scenarios.
What I think is happening is that in fragment you are calling .collect { } and that is ensuring the flow is started. Check the Terminal operator definition: Terminal operators on flows are suspending functions that start a collection of the flow. https://kotlinlang.org/docs/flow.html#terminal-flow-operators
This is not true for sharedFlow, which might be configured to be started eagerly.
So to solve your issue, you might just call
val job = launch {
trendingSearchViewModel.trendingSearchStateFlow.collect()
}
This is what worked for me:
#Test
fun `should get trending search suggestions`() {
runBlockingTest {
// Arrange
val trending1 = UUID.randomUUID().toString()
val trending2 = UUID.randomUUID().toString()
val trending3 = UUID.randomUUID().toString()
val listOfTrending = listOf(trending1, trending2, trending3)
whenever(loadTrendingSearchUseCase.execute()).thenReturn(listOfTrending)
/* List to collect the results */
val listOfEmittedResult = mutableListOf<List<String>>()
val job = launch {
trendingSearchViewModel.trendingSearchStateFlow.toList(listOfEmittedResult)
}
// Act
trendingSearchViewModel.getTrendingSearch()
// Assert
assertThat(listOfEmittedResult).isNotEmpty()
verify(loadTrendingSearchUseCase).execute()
job.cancel()
}
}

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

Best way to execute custom logic when setting value to MutableLiveData

What is the most recommended approach to execute custom logic when setting value of MutableLiveData?
I have a ViewModel with several properties isConnecting and isConnected.
I want to set isConnecting to false when isConected is changed
class MyViewModel : ViewModel() {
private var _isConnecting = MutableLiveData<Boolean>()
val isConnecting: LiveData<Boolean>
get () = _isConnecting
private var _isConnected = MutableLiveData<Boolean>()
val isConnected: LiveData<Boolean>
get () = _isConnected
}
One way to do it is creating a function inside MyViewModel and set both properties:
fun setConnected(value: Boolean) {
_isConnected.value = value
_isConnecting.value = false
}
This is okay, but one must never set _isConnected manually and always use function setConnected(). It can not be guaranteed and thus there may be bugs.
Another way to do it is to make MyViewModel observe its own MutableLiveData:
class MyViewModel : ViewModel() {
// ...
private val isConnectedObserver = Observer<Boolean> {
_isConnecting.value = false
}
init {
isConnected.observeForever(isConnectedObserver)
}
override fun onCleared() {
super.onCleared()
isConnected.removeObserver(isConnectedObserver)
}
}
This avoids problem of first approach, but is just awful.
But is there a better way? For example using setters somehow?
Use MediatorLiveData to observe other LiveData objects and react on onChanged events from them:
class MyViewModel : ViewModel() {
private var _isConnecting = MediatorLiveData<Boolean>().also { liveData ->
liveData.addSource(_isConnected) { connected ->
// isConnected changed, some logic here
if (connected) {
liveData.value = false
}
}
}
val isConnecting: LiveData<Boolean>
get() = _isConnecting
private var _isConnected = MutableLiveData<Boolean>()
val isConnected: LiveData<Boolean>
get() = _isConnected
}

Kotlin: Unit testing a function that takes a function as parameter

I have a function that retrieves a list of items from a repository. Instead of using a regular callback I pass in a function and invoke this with the result. But how can you unittest this kind of function. Is there some way to verify that the passed in function is being invoked or should I refactor and use a regular callback and test it using a mocked callback-interface?
My code:
class WorklistInteractor #Inject
constructor(private val worklistRepository: WorklistRepository,
private val preferenceManager: PreferenceManager,
private val executor: Executor)
: WorklistDialogContract.Interactor, Executor by executor {
#Volatile private var job: Job? = null
override fun getWorklist(callback: (Result<List<WorklistItem>>) -> Unit) {
job = onWorkerThread {
val result = worklistRepository.getWorklist().awaitResult()
onMainThread { callback(result) }
}
}
override fun cancel() {
job?.cancel()
}
}
To check it is called, something like that would work:
var hasBeenCalled = false
interactor.getWorklist({ result -> hasBeenCalled = true })
assertTrue(hasBeenCalled)
Of course you could also check that the expected result is passed, etc.
You can simply pass a callback function that sets a test-local variable which you verify as an assertion. For simplicity, I changed the example a bit:
fun getWorklist(callback: (String) -> Unit) = callback("helloWorld")
#Test
fun testCase() {
var invoked = false
getWorklist {
invoked = true
it.length
}
Assert.assertTrue(invoked)
}

Categories

Resources