I am writing unit tests and I have this particular case when I change observable value before executing a suspend function and then right after. I would like to write a unit test to check if the value was changes twice correctly.
Here is the method:
fun doSomething() {
viewModelScope.launch(ioDispatcher) {
try {
viewModelScope.launch(Dispatchers.Main) {
commandLiveData.value = Pair(CANCEL_TIMER, null)
}
isProgress.set(true) //need to test it was set to true in one test
checkoutInteractor.doSomethingElse().run {
handleResult(this)
}
isProgress.set(false) //need to test it was set to false here in another test
} catch (ex: java.lang.Exception) {
handleHttpError(ex)
isProgress.set(false)
}
}
}
When writing a test I am calling doSomething() but I am unsure how to detect that the value was set to true before the checkoutInteractor.doSomethingElse call and set to false after.
Here is the test I have
#Test
fun `test doSomething enables progress`() {
runBlockingTest {
doReturn(Response()).`when`(checkoutInteractor). checkoutInteractor.doSomethingElse()
viewModel.doSomething()
assertTrue(viewModel.isProgress.get()) //fails obviously as the value was already set to false after the `doSomethingElse` was executed.
}
}
BTW, isProgress is an ObservableBoolean
You would need to either mock or spy on isProgress and checkoutInteractor fields to record and verify the execution of their methods.
Pass a mock or spy for isProcess and checkoutInteractor into your class.
Execute doSomething()
Verify inOrder the set() and doSomethingElse() functions
Example:
#Test
fun `test doSomething enables progress`() {
runBlockingTest {
val checkoutInteractor = Mockito.spy(CheckoutInteractor())
val isProcess = Mockito.spy(ObservableBoolean(true))
val viewModel = ViewModel(isProcess, checkoutInteractor)
viewModel.doSomething()
val inOrder = inOrder(isProcess, checkoutInteractor)
inOrder.verify(isProcess).set(true)
inOrder.verify(checkoutInteractor).doSomethingElse()
inOrder.verify(isProcess).set(false)
}
}
Related
I have a ViewModel that uses StateFlow.asLiveData() to expose a Repository class's StateFlow items and I'm trying to write a test for the ViewModel. My tests are configured with a Mock of an Observer<LoadingStatus> on the ViewModel's exposed LiveData.
The code I'm testing calls this method to update its loading status:
suspend fun MutableStateFlow<LoadingStatus>.performWithStatusUpdates(operation: suspend () -> Unit) {
this.value = LoadingStatus.Loading()
try {
operation.invoke()
this.value = LoadingStatus.Success()
} catch (e: Throwable) {
this.value = LoadingStatus.Error(e)
}
}
My tests look something like this:
fun testSomething() = runTest {
viewModel.doSomething()
advanceUntilIdle()
argumentCaptor<LoadingStatus>().apply {
verify(loadingStatusObserver, atLeast(2)).onChanged(capture())
assertTrue(allValues.any { it is LoadingStatus.Loading })
assertTrue(allValues.any { it is LoadingStatus.Success })
}
}
The ViewModel contains code like this:
val loadingStatus = repository.loadingStatusObservable.asLiveData(
viewModelScope.coroutineContext + ioDispatcher
) // when running tests, ioDispatcher is a StandardTestDispatcher passed into the viewModel
fun doSomething() {
viewModelScope.launch(ioDispatcher) {
repository.doSomething()
}
}
And the repository does something like this:
val loadingStatusObservable = MutableStateFlow<LoadingStatus>(LoadingStatus.Idle())
suspend fun doSomething() {
loadingStatusObservable.performWithStatusUpdates {
apiService.doSomethingElse()
}
}
The repository has similar tests that call doSomething() and verify that the status goes to loading and then success, and they pass, but the view model ones fail to pick up the Loading status. If I comment out the line in performWithStatusUpdates that sets the status to success after the operation, the tests do pick up the Loading status, so I'm convinced it's something to do with timing. I have run this code with print statements and debuggers and verified that the status is updating correctly, but the change isn't getting picked up by the observers.
How can I make an observer on a StateFlow.asLiveData() detect all changes, even when they're quickly followed by another, different change?
I have Unit test function RxJava with timeout but it doesn't subscribe for unit test.
Function on viewModel
fun loadData() {
loadDataUseCase.loadData(true)
.subscribeOn(Schedulers.io())
.timeout(30L, TimeUnit.SECONDS, schedulers)
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe {
onShowLoading.value = true
onShowError.value = false
onShowContent.value = false
}.subscribe(
{
onConnected.value = true
onShowContent.value = true
onShowError.value = false
onShowLoading.value = false
},
{
onShowError.value = true
onShowLoading.value = false
onShowContent.value = false
}
)
.addTo(compositeDisposable)
}
Function on unit test
#Test
fun `Load data is success`() {
// given
whenever(loadDataUseCase.loadData(true)).thenReturn(Observable.just(true))
// when
viewModel.loadData()
// then
viewModel.onShowError().test().assertValue(false).awaitNextValue().assertValue(false)
}
I try to debug this function but it doesn't invoke subscribe
I am a bit confused, what is onShowError() does it return a LiveData?
If I run the same code the test doesn't even finish (well I use only io dispatchers and postValue), for you it might be finished before the subscription even happens:
Since you rely on Schedulers.io() it is possible that your whole Subscription is finished before you get to even test your LiveData.
An other option is that your LiveData already has a false value: .assertValue(false). then the next .doOnSubscribe setting already triggers .awaitNextValue() and your whole test finishes, before the subscription can even be called.
Your tests should be fixed and not dependent on timing. Or if it is unavoidable then you have to synchronize your test somehow, an example of this is here:
#Timeout(1, unit = TimeUnit.SECONDS)
#Test
fun `Load data is success`() {
// given
whenever(loadDataUseCase.loadData(true)).thenReturn(Observable.just(true)
val testObserver = liveData.test()
testObserver.assertNoValue() // assert in correct state before actions
// when
loadData(liveData, mock)
//then
testObserver.awaitValueHistorySize(2)
.assertValueHistory(false, false)
}
fun <T> TestObserver<T>.awaitValueHistorySize(count: Int, delay: Long = 10): TestObserver<T> {
awaitValue()
while (valueHistory().size < count) {
// we need to recheck and don't block, the value we are trying to wait might already arrived between the while condition and the awaitNextValue in the next line, so we use some delay instead.
awaitNextValue(delay, TimeUnit.MILLISECONDS)
}
return this
}
I have a suspendCoroutine in my repository with which I want to send data back to my ViewModel -
suspend fun sendPasswordResetMail(emailId: String): Boolean {
return withContext(Dispatchers.IO) {
suspendCoroutine { cont ->
firebaseAuth?.sendPasswordResetEmail(emailId)
?.addOnCompleteListener {
cont.resume(it.isSuccessful)
}
?.addOnFailureListener {
cont.resumeWithException(it)
}
}
}
}
However, neither of the listeners are called. Debugger says no executable code found at line where 'cont.resume(it.isSuccessful)' or 'cont.resumeWithException(it)' are.
I tried 'Dispatchers.IO', 'Dispatchers.Main' and 'Dispatchers.Default' but none of them seem to work. What could I be doing wrong?
My ViewModel code -
isEmailSent : LiveData<Boolean> = liveData {
emit(firebaseAuthRepo.sendPasswordResetMail(emailId))
}
and
fragment -
viewModel.isEmailSent.observe(viewLifecycleOwner, { flag ->
onResetMailSent(flag)
})
I believe you are calling
isEmailSent : LiveData<Boolean> = liveData {
emit(firebaseAuthRepo.sendPasswordResetMail(emailId))
}
this piece of code everytime for sending email
and
viewModel.isEmailSent.observe(viewLifecycleOwner, { flag ->
onResetMailSent(flag)
})
this piece only once.
Assuming that's true what you are essentially observing is the initial live data that was created with the model while it is being replaced everytime when resent is called. Instead call
isEmailSent.postValue(firebaseAuthRepo.sendPasswordResetMail(emailId))
from inside of a coroutine.
Also for the debugger not showing anything try adding a log above the cont.resume call and cont.resumeWithException call since it has worked for me in the past.
I think the easier way to achieve this is by using firebase-ktx and the await() function (which does what you are trying under the hood):
suspend fun sendPasswordResetMail(emailId: String): Boolean {
try {
firebaseAuth?.sendPasswordResetEmail(emailId).await()
return true
} catch(e: Exception) {
return false
}
}
Another way would be to use flow:
suspend fun sendPasswordResetMail(emailId: String): Boolean = flow<Boolean {
firebaseAuth?.sendPasswordResetEmail(emailId).await()
emit(true)
}.catch { e: Exception -> handleException(e) }
You could then observe this in your fragment by putting the code inside your viewmodel and calling .asLiveData()
I've been reading this article to understand how to unit test a coroutine that contains a delay and applied it, but I still don't understand why verify is being called before having called myDelayedMethod() in the coroutine and therefore the verification fails. Isn't there a way to execute the code synchronously in the test?
Pseudocode:
class ClasUnderTest{
fun method1(){
GlobalScope.launch {
myDelayedMethod()
}
}
suspend fun myDelayedMethod(): String{
withContext(dispatchers.default()){
delay(X)
...
someClass.someMethod()
}
}
}
#Test
fun myTest()= coroutinesTestRule.testDispatcher.runBlockingTest {
val someClassMock = mock(SomeClass::class.java)
val myObject = ClasUnderTest(someClassMock)
method1()
verify(someClassMock).someMethod()
}
One idea could be to return the Job in method1 like the following:
fun method1(): Job {
return GlobalScope.launch {
myDelayedMethod()
}
}
And then replace method1() with method1().join(), so that runBlockingTest waits for the execution of the coroutine.
This function is executed from my activity to check if a specific feature flag is enable, dispatcher.io() is injected so, for test mode the dispatcher is Main
fun isFeatureFlagEnabled(nameKey: String, completion: (enabled: Boolean) -> Unit) {
CoroutineScope(context = dispatcher.io()).launch {
val featureFlag = featureFlagsRepository.featureFlagsDao.getFeatureFlag(nameKey)
if (featureFlag != null) {
completion(featureFlag.isEnabled)
}
else {
completion(false)
}
}
}
This is the invocation if the function in the activity
private fun launchClearentCardOnFileBaseOnFeatureFlag(cardNavHelper: CardNavHelper){
featureFlagsHelper?.isFeatureFlagEnabled(CLEARENT_CARD_ON_FILE){enable ->
if(enable){
putFragment(
ClearentNativeFormFragment.newInstance(cardNavHelper))
}else{
putFragment(
ClearentCardOnFileFormFragment.newInstance(cardNavHelper))
}
}
}
And this is my test that fails because Espresso doesn't wait for the lambda function that i pass as parameter to return a response after check the database (it is an inMemoryDatabase)
#Test
fun testFFClearentNativeFormEnable(){
mockWebServer.setDispatcher(BusinessWithAddonsAndPaymentProcessorClearentDispatcher())
val application = ApplicationProvider.getApplicationContext<SchedulicityApplication>()
runBlocking(Dispatchers.Main) {
application.featureFlagsHelper.featureFlagsRepository.featureFlagsDao.insertFeatureFlags(
featureFlagsEnable
)
}
intent.putExtra(Constants.INTENT_KEY_CARD_ON_FILE_EXTRAS, cardNavHelperFromClientRecord)
activityTestRule.launchActivity(intent)
runBlocking { delay(10000) }
populateClearentForm(validCardNumber = false, validExpDate = true)
}
I have to put this delay runBlocking { delay(10000) } other ways fails.
So my question is. Do you know how i can wait for the coroutine response in the UI thread so my test could pass ?
Have a look at the espresso idling resources.