Mockito Says Actual invocation has different arguments when using MutableLiveData Value - android

Please Read the whole description I have tried my best to explain every corner of the code.
Mockito Says Actual invocation has different arguments
I am testing a update passwrod function which has a code like this ,
fun update() {
if (uiModel.validateData().isEmpty()) {
changePassword(
uiModel.oldpassword.value ?: "",
uiModel.newpassword.value ?: "",
uiModel.confirmpassword.value ?: ""
)
} else {
showToast(uiModel.validateData()[0])
}
}
uiModel.validateData().isEmpty() this line validate the user input if it is ok then it proceed inside of the block.
uiModel.oldpassword.value uiModel is my supporting class which has a Mutablelive data which is connected via databinding to EditTextView which gets and sets data into view. I have already mocked the uiModel class which is a simple class provided by dagger in viewModel constructor here is uiModel class code.
class ChangePasswordUiModel #Inject constructor() {
var oldpassword = MutableLiveData<String>().default("")
var newpassword= MutableLiveData<String>().default("")
var confirmpassword= MutableLiveData<String>().default("")
}
and here is the viewModel injection setup
#ChangePasswordScope
class ChangePasswordViewModel #Inject constructor(
private val useCase: ChangePasswordUseCase,
val uiModel: ChangePasswordUiModel
) : BaseFragmentViewModel() {
}
If validation is all set then this function gets called.
changePassword(uiModel.oldpassword.value ?: "",
uiModel.newpassword.value ?: "",
uiModel.confirmpassword.value ?: ""
)
which is actually looks like this.
var testValue = ""
private fun changePassword(oldpass: String, newpass: String, confirmPass: String) {
viewModelScope.launch {
useCase.changePassword(
oldpass,
newpass,
confirmPass
).let { result ->
when (result) {
is Result.Success -> {
testValue = "Success"
}
is Result.Exception -> showException(result.exception)
is Result.Error -> showError(parseError(result.errorBody))
else -> {
}
}
}
}
}
Now useCase.changePassword() this function actually does magic for me which actually initiates network request and return me a custom sealed class which has three values Success(Any()), Error(), Exception.
UseCase looks like this.
interface ChangePasswordUseCase {
suspend fun changePassword(oldpassword: String, newpassword: String, confirmpassword: String): Result
}
NOW THE PROBLEM WITH TESTING
I want to check after update() function invocation that changepassword is invoked or not
and my testing code looks like this,
// these are the values which set up in #before
val useCase = mock<ChangePasswordUseCase>()
val uiModel = mock<ChangePasswordUiModel>()
val SUT: ChangePasswordViewModel by lazy { ChangePasswordViewModel(useCase, uiModel) }
#Test
fun `update pass validate pass and change pass`() {
val emptyLiveData = MutableLiveData("abc")
whenever(uiModel.validateData()).thenReturn(mutableListOf())
whenever(uiModel.oldpassword).thenReturn(emptyLiveData)
whenever(uiModel.confirmpassword).thenReturn(emptyLiveData)
whenever(uiModel.newpassword).thenReturn(emptyLiveData)
runBlockingTest {
whenever(
useCase.changePassword(
(emptyLiveData.value!!),
(emptyLiveData.value!!),
(emptyLiveData.value!!)
)
).thenReturn(
Result.Success(
ChangePasswordResponse()
)
)
SUT.update()
verify(useCase).changePassword(
(emptyLiveData.value!!),
(emptyLiveData.value!!),
(emptyLiveData.value!!)
)
assertThat(SUT.testValue).isEqualTo("Success")
}
}
whenever is my extension function written on Mockito.when
and finally here is the problem which took my day but not solved ... I know the problem is values reference issue but I dont know how I solve this problem
ERROR
Argument(s) are different! Wanted:
changePasswordUseCase.changePassword(
"abc",
"abc",
"abc",
Continuation at com.trainerhub.trainer.view.home.view.change_password.ChangePasswordViewModelTest$update pass validate pass and change pass$1.invokeSuspend(ChangePasswordViewModelTest.kt:105)
);
-> at com.trainerhub.trainer.view.home.view.change_password.ChangePasswordViewModelTest$update pass validate pass and change pass$1.invokeSuspend(ChangePasswordViewModelTest.kt:102)
Actual invocation has different arguments:
changePasswordUseCase.changePassword(
"abc",
"abc",
"abc",
Continuation at com.trainerhub.trainer.view.home.view.change_password.ChangePasswordViewModel$changePassword$1.invokeSuspend(ChangePasswordViewModel.kt:61)
);
-> at com.trainerhub.trainer.view.home.view.change_password.ChangePasswordViewModel$changePassword$1.invokeSuspend(ChangePasswordViewModel.kt:58)
Comparison Failure:
<Click to see difference>
Argument(s) are different! Wanted:
changePasswordUseCase.changePassword(
"abc",
"abc",
"abc",
Continuation at com.trainerhub.trainer.view.home.view.change_password.ChangePasswordViewModelTest$update pass validate pass and change pass$1.invokeSuspend(ChangePasswordViewModelTest.kt:105)
);
-> at com.trainerhub.trainer.view.home.view.change_password.ChangePasswordViewModelTest$update pass validate pass and change pass$1.invokeSuspend(ChangePasswordViewModelTest.kt:102)
Actual invocation has different arguments:
changePasswordUseCase.changePassword(
"abc",
"abc",
"abc",
Continuation at com.trainerhub.trainer.view.home.view.change_password.ChangePasswordViewModel$changePassword$1.invokeSuspend(ChangePasswordViewModel.kt:61)
);
-> at com.trainerhub.trainer.view.home.view.change_password.ChangePasswordViewModel$changePassword$1.invokeSuspend(ChangePasswordViewModel.kt:58)
at com.trainerhub.trainer.view.home.view.change_password.ChangePasswordViewModelTest$update pass validate pass and change pass$1.invokeSuspend(ChangePasswordViewModelTest.kt:102)
at com.trainerhub.trainer.view.home.view.change_password.ChangePasswordViewModelTest$update pass validate pass and change pass$1.invoke(ChangePasswordViewModelTest.kt)
at kotlinx.coroutines.test.TestBuildersKt$runBlockingTest$deferred$1.invokeSuspend(TestBuilders.kt:50)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.test.TestCoroutineDispatcher.dispatch(TestCoroutineDispatcher.kt:50)
at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:305)
at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:30)
at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable$default(Cancellable.kt:27)
at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109)
at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:158)
at kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt:91)
at kotlinx.coroutines.BuildersKt.async(Unknown Source)
at kotlinx.coroutines.BuildersKt__Builders_commonKt.async$default(Builders.common.kt:84)
at kotlinx.coroutines.BuildersKt.async$default(Unknown Source)
at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:49)
at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest$default(TestBuilders.kt:45)
at com.trainerhub.trainer.view.home.view.change_password.ChangePasswordViewModelTest.update pass validate pass and change pass(ChangePasswordViewModelTest.kt:88)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:61)
at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:61)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.CommandLineWrapper.main(CommandLineWrapper.java:64)
Process finished with exit code -1

everything looks perfect in your code.
when working with coroutines and suspend functions, you will need both coroutines test and mockito-kotlin dependencies.
make sure you have both dependencies added in your build.gralde.
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2'
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"

Related

Koin in testing: No definition found for class:'okhttp3.mockwebserver.MockWebServer' (org.koin.core.error.NoBeanDefFoundException)

I am trying to write some Koin API tests, but they fail to execute because okhttp3 MockWebServer bean can not be found.
I am using:
Koin Core features: implementation "io.insert-koin:koin-core:3.2.0"
Koin Test features: testImplementation "io.insert-koin:koin-test:3.2.0"
These are my setup/teardown methods:
companion object {
const val TEST_SCOPE = "TEST_SCOPE"
}
#Before
fun initTest() {
stopKoin()
val modules = listOf(module {
scope(named(TEST_SCOPE)) { MockWebServer() }
factory {
AuthenticationManager(
get<MockWebServer>().url("").toString()
)
}
})
startKoin {
koinApplication { allowOverride(true) }
loadKoinModules(modules)
}
getKoin().createScope("AuthTestScope", named(TEST_SCOPE))
}
#After
fun shutdown() {
get<MockWebServer>().shutdown()
getKoin().getScope(TEST_SCOPE).close()
stopKoin()
}
And this is a test example:
#Test
fun `login sends proper body`() {
get<MockWebServer>().apply {
enqueue(MockResponse().setBody(MockResponseFileReader("auth_success.json").content))
}
val authManager = get<AuthenticationManager>().apply {
authenticateBlocking()
}
val testBody = LoginBody(AuthenticationManager.email, AuthenticationManager.password)
val requestBody =
URLDecoder.decode(get<MockWebServer>().takeRequest().body.readUtf8(), "UTF-8")
val requestParams = getParametersFromRequestBody(requestBody)
val requestEmail = requestParams[0]
val requestPassword = requestParams[1]
assertEquals(requestEmail, testBody.email)
assertEquals(requestPassword, testBody.password)
}
Tests themselves aren't that important since they break in the very first line when they try to get the MockWebServer.\
Full error log:
|- No definition found for class:'okhttp3.mockwebserver.MockWebServer'. Check your definitions!
org.koin.core.error.NoBeanDefFoundException: |- No definition found for class:'okhttp3.mockwebserver.MockWebServer'. Check your definitions!
at app//org.koin.core.scope.Scope.throwDefinitionNotFound(Scope.kt:305)
at app//org.koin.core.scope.Scope.resolveValue(Scope.kt:275)
at app//org.koin.core.scope.Scope.resolveInstance(Scope.kt:242)
at app//org.koin.core.scope.Scope.get(Scope.kt:205)
at app//com.riteh.autoshare.AuthenticationManagerTest.registration sends proper body(AuthenticationManagerTest.kt:178)
at java.base#11.0.12/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base#11.0.12/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base#11.0.12/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base#11.0.12/java.lang.reflect.Method.invoke(Method.java:566)
at app//org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
at app//org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at app//org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at app//org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at app//org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at app//org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
at app//org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at app//org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
at app//org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at app//org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at app//org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
at app//org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at app//org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at app//org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at app//org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at app//org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at app//org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at app//org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.runTestClass(JUnitTestClassExecutor.java:110)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:58)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:38)
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestClassProcessor.processTestClass(AbstractJUnitTestClassProcessor.java:62)
at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:51)
at java.base#11.0.12/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base#11.0.12/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base#11.0.12/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base#11.0.12/java.lang.reflect.Method.invoke(Method.java:566)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
at com.sun.proxy.$Proxy2.processTestClass(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:176)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:133)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:71)
at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
Any help is appreciated. I guess this could be solved by switching to older Koin versions and some code refactoring, but I'm trying to see if there's a simpler solution that I'm missing.

CoroutineExceptionHandler doesn't work with ViewModelScope and Koin

I have a method to fetch devices using coroutines and use viewModelScope.launch to run coroutines. I want to catch errors using CoroutineExceptionHandler, but I get the error:
E/[Koin]: Instance creation error : could not create instance for [Factory:'com.test.presentation.viewModel.ActivateDeviceViewModel']: java.lang.NullPointerException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkNotNullParameter, parameter context
kotlin.coroutines.CombinedContext.plus(Unknown Source:2)
kotlinx.coroutines.CoroutineContextKt.newCoroutineContext(CoroutineContext.kt:33)
kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders.common.kt:52)
kotlinx.coroutines.BuildersKt.launch(Unknown Source:1)
kotlinx.coroutines.BuildersKt__Builders_commonKt.launch$default(Builders.common.kt:47)
kotlinx.coroutines.BuildersKt.launch$default(Unknown Source:1)
com.test.presentation.viewModel.ActivateDeviceViewModel.fetchDevices(ActivateDeviceViewModel.kt:42)
com.test.presentation.viewModel.ActivateDeviceViewModel.<init>(ActivateDeviceViewModel.kt:29)
com.test.di.ModulesKt$viewModelModule$1$2.invoke(Modules.kt:63)
com.test.di.ModulesKt$viewModelModule$1$2.invoke(Modules.kt:63)
org.koin.core.instance.InstanceFactory.create(InstanceFactory.kt:51)
org.koin.core.instance.FactoryInstanceFactory.get(FactoryInstanceFactory.kt:36)
org.koin.core.registry.InstanceRegistry.resolveInstance$koin_core(InstanceRegistry.kt:103)
org.koin.core.scope.Scope.resolveInstance(Scope.kt:236)
org.koin.core.scope.Scope.access$resolveInstance(Scope.kt:34)
org.koin.core.scope.Scope$get$1.invoke(Scope.kt:199)
org.koin.core.time.MeasureKt.measureDurationForResult(Measure.kt:75)
org.koin.core.scope.Scope.get(Scope.kt:198)
com.test.presentation.fragment.ActivateDeviceFragment$special$$inlined$inject$default$1.invoke(ComponentCallbackExt.kt:69)
kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74)
com.test.presentation.fragment.ActivateDeviceFragment.getViewModel(ActivateDeviceFragment.kt:39)
com.test.presentation.fragment.ActivateDeviceFragment.access$getViewModel(ActivateDeviceFragment.kt:37)
com.test.presentation.fragment.ActivateDeviceFragment$handleActivateDeviceResult$1$1.invokeSuspend(ActivateDeviceFragment.kt:51)
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
androidx.lifecycle.DispatchQueue.drainQueue(DispatchQueue.kt:75)
androidx.lifecycle.DispatchQueue.resume(DispatchQueue.kt:54)
androidx.lifecycle.LifecycleController$observer$1.onStateChanged(LifecycleController.kt:40)
androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:354)
androidx.lifecycle.LifecycleRegistry.forwardPass(LifecycleRegistry.java:265)
androidx.lifecycle.LifecycleRegistry.sync(LifecycleRegistry.java:307)
androidx.lifecycle.LifecycleRegistry.moveToState(LifecycleRegistry.java:148)
androidx.lifecycle.LifecycleRegistry.handleLifecycleEvent(LifecycleRegistry.java:134)
androidx.fragment.app.Fragment.performStart(Fragment.java:3024)
androidx.fragment.app.FragmentStateManager.start(FragmentStateManager.java:568)
androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:277)
androidx.fragment.app.FragmentStore.moveToExpectedState(FragmentStore.java:113)
androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1327)
androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:2757)
androidx.fragment.app.FragmentManager.dispatchStart(FragmentManager.java:2707)
androidx.fragment.app.Fragment.performStart(Fragment.java:3028)
androidx.fragment.app.FragmentStateManager.start(FragmentStateManager.java:568)
androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:277)
androidx.fragment.app.FragmentStore.moveToExpectedState(FragmentStore.java:113)
androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1327)
androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:2757)
androidx.fragm.
Please tell me what am I doing wrong and how to fix it? Thanks
fragment
class ActivateDeviceFragment : Fragment(R.layout.fragment_activate_device) {
private val viewModel: ActivateDeviceViewModel by inject()
// ....
}
view model
class ActivateDeviceViewModel constructor(
private val activateDeviceUseCase: ActivateDeviceUseCase,
) : ViewModel() {
init {
fetchDevices()
}
private val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Timber.tag("test").d(throwable)
}
private fun fetchDevices() {
viewModelScope.launch(exceptionHandler) {
}
}
}
koin modules
val useCaseModule = module {
single { ActivateDeviceUseCase() }
}
val viewModelModule = module {
viewModel { ActivateDeviceViewModel(get() }
}
It is not related to Koin.
The log shows that the exceptionHandler in viewModelScope.launch(exceptionHandler) is actually null, and which throws NullPointerException.
It is because you trigger the fetchDevices() inside init{}, in which the instantiation of exceptionHandler is not ready (means it is null).
Therefore, you should not call fetchDevices() inside init{}. For example, you could call this in your Fragment onViewCreated(){ viewModel.fetchDevices } instead. It will be fine.

Android: Impossible NullPointerException when using viewModelscope and withContext

My problem is, that I get an impossible NullPointerException. When I access my emailEntity data from my price variable without using an elvis-operator, my price variable gets null and I get a NullPointerException.
Now comes the problem: When I use an elvis-operator at my price variable and access my emailEntity data within a function, I do not get a NullPointerException and the price is correctly set. What am I doing wrong?
Base Code
class EmailViewModel #ViewModelInject constructor() : ViewModel() {
// This is the value I access from my price variable and the function
private val emailEntity = MutableLiveData<EmailEntity?>()
// Setting the value of my emailEntity here
init {
// I have to use viewModelScope because "getCalibratePrice and getRepairPrice" are suspend functions
viewModelScope.launch {
withContext(Dispatchers.IO) {
when(subject.value.toString()) {
"Toast" -> emailEntity.postValue(emailRepository.getCalibratePrice())
else -> emailEntity.postValue(emailRepository.getRepairPrice())
}
}
}
}
}
Problem Code
// NullPointerException
val price = MutableLiveData(emailEntity.value?.basePrice!!)
fun checkIfPriceIsInitialized() {
Timber.d("Emailprice is ${emailEntity.value.basePrice}")
}
Working Code
// NO NullPointerException but value is now always 0F
val price = MutableLiveData(emailEntity.value?.basePrice ?: 0F)
// EmailEntity price is correctly set here!!!
fun checkIfPriceIsInitialized() {
Timber.d("Emailprice is ${emailEntity.value.basePrice}")
}
StackTrace
java.lang.NullPointerException
at com.example.app.framework.ui.viewmodel.EmailViewModel.<init>(EmailViewModel.kt:164)
at com.example.app.framework.ui.viewmodel.EmailViewModel_AssistedFactory.create(EmailViewModel_AssistedFactory.java:58)
at com.example.app.framework.ui.viewmodel.EmailViewModel_AssistedFactory.create(EmailViewModel_AssistedFactory.java:20)
at androidx.hilt.lifecycle.HiltViewModelFactory.create(HiltViewModelFactory.java:76)
at androidx.lifecycle.AbstractSavedStateViewModelFactory.create(AbstractSavedStateViewModelFactory.java:69)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:185)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:150)
at androidx.lifecycle.ViewModelLazy.getValue(ViewModelProvider.kt:54)
at androidx.lifecycle.ViewModelLazy.getValue(ViewModelProvider.kt:41)
at com.example.app.framework.ui.view.fragments.home.calibrateAndRepair.CalibrateRepairMessageFragment.getViewModel(Unknown Source:2)
at com.example.app.framework.ui.view.fragments.home.calibrateAndRepair.CalibrateRepairMessageFragment.getViewModel(CalibrateRepairMessageFragment.kt:26)
at com.example.app.framework.ui.view.basefragments.BaseFragment.onCreateView(BaseFragment.kt:30)
at com.example.app.framework.ui.view.basefragments.EmailFragment.onCreateView(EmailFragment.kt:54)
at androidx.fragment.app.Fragment.performCreateView(Fragment.java:2699)
at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:320)
at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1199)
at androidx.fragment.app.FragmentManager.addAddedFragments(FragmentManager.java:2236)
at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2009)
at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1965)
at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1861)
at androidx.fragment.app.FragmentManager$4.run(FragmentManager.java:413)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
EmailViewModel.<init>(EmailViewModel.kt:164) points to -> val price = MutableLiveData(emailEntity.value?.basePrice!!)
Please bear in mind that I started with kotlin coroutines from scratch. Therefore I do not know 100% how it all really works
EDIT
This is my repository:
interface EmailRepository {
fun sendEmail(email: Email): Flow<EmailStatus<Unit>>
suspend fun getCalibratePrice(): Flow<EmailEntity?>
suspend fun getRepairPrice(): Flow<EmailEntity?>
}
And this is my implementation:
class EmailRepositoryImpl #Inject constructor(
private val db: FirebaseFirestore
) : EmailRepository {
override suspend fun getCalibratePrice(): Flow<EmailEntity?> = flow {
val result = db.collection("emailprice").document("Kalibrieren").get().await()
val emailEntity = result.toObject<EmailEntity?>()
emit(emailEntity)
}.catch {
Timber.d("Error on getCalibrate Price")
}.flowOn(Dispatchers.Main)
override suspend fun getRepairPrice(): Flow<EmailEntity?> = flow {
val collection = db.collection("emailprice").document("Reparieren").get().await()
val emailEntity = collection.toObject<EmailEntity?>()
emit(emailEntity)
}.catch {
Timber.d("Error on getRepairPrice")
}.flowOn(Dispatchers.Main)
}
The alternative would be to use .single() at the end and change the return type from Flow<EmailEntity?> to EmailEntity
EDIT 2
private var emailEntity: EmailEntity = EmailEntity("", 50F)
init {
viewModelScope.launch {
when(subject.value.toString()) {
context.getString(R.string.home_calibrate_card_headline) -> emailRepository.getCalibratePrice().collect {
emailEntity = it ?: EmailEntity("Error", 100F)
}
else -> emailRepository.getRepairPrice().collect {
emailEntity = it ?: EmailEntity("Error Zwei", 150F)
}
}
}
}
// Price is 50 and does not change..
val price = MutableLiveData(emailEntity.basePrice)
Your coroutine is running asynchronous code. By the time your EmailViewModel is instantiated, the coroutine hasn't finished running yet, so at that point, the value of the LiveData is still null. You must be trying to retrieve the value immediately from your main thread, before the coroutine finishes running.
Typically with a LiveData, you almost never retrieve a value directly. Instead, you should observe the LiveData with a callback so you can react when it gets a value, which is not going to happen immediately with suspend functions and coroutines.
By the way, you should only update LiveData from the main thread, which you are failing to do with your coroutine. Assuming your suspend functions properly delegate to background threads, which is the case if they're from you using the Room library, you should remove the wrapping withContext block from your coroutine. A properly composed suspend function can always be called from the Main Dispatcher safely, and will delegate to background dispatchers as necessary internally.

Unit testing a Kotlin coroutine with delay

I'm trying to unit test a Kotlin coroutine that uses delay(). For the unit test I don't care about the delay(), it's just slowing the test down. I'd like to run the test in some way that doesn't actually delay when delay() is called.
I tried running the coroutine using a custom context which delegates to CommonPool:
class TestUiContext : CoroutineDispatcher(), Delay {
suspend override fun delay(time: Long, unit: TimeUnit) {
// I'd like it to call this
}
override fun scheduleResumeAfterDelay(time: Long, unit: TimeUnit, continuation: CancellableContinuation<Unit>) {
// but instead it calls this
}
override fun dispatch(context: CoroutineContext, block: Runnable) {
CommonPool.dispatch(context, block)
}
}
I was hoping I could just return from my context's delay() method, but instead it's calling my scheduleResumeAfterDelay() method, and I don't know how to delegate that to the default scheduler.
If you don't want any delay, why don't you simply resume the continuation in the schedule call?:
class TestUiContext : CoroutineDispatcher(), Delay {
override fun scheduleResumeAfterDelay(time: Long, unit: TimeUnit, continuation: CancellableContinuation<Unit>) {
continuation.resume(Unit)
}
override fun dispatch(context: CoroutineContext, block: Runnable) {
//CommonPool.dispatch(context, block) // dispatch on CommonPool
block.run() // dispatch on calling thread
}
}
That way delay() will resume with no delay. Note that this still suspends at delay, so other coroutines can still run (like yield())
#Test
fun `test with delay`() {
runBlocking(TestUiContext()) {
launch { println("launched") }
println("start")
delay(5000)
println("stop")
}
}
Runs without delay and prints:
start
launched
stop
EDIT:
You can control where the continuation is run by customizing the dispatch function.
In kotlinx.coroutines v1.6.0 the kotlinx-coroutines-test module was updated. It allows tests to use the runTest() method and TestScope to test suspending code, automatically skipping delays.
See the documentation for details on how to use the module.
Previous Answer
In kotlinx.coroutines v1.2.1 they added the kotlinx-coroutines-test module. It includes the runBlockingTest coroutine builder, as well as a TestCoroutineScope and TestCoroutineDispatcher. They allow auto-advancing time, as well as explicitly controlling time for testing coroutines with delay.
Use TestCoroutineDispatcher, TestCoroutineScope, or Delay
TestCoroutineDispatcher, TestCoroutineScope, or Delay can be used to handle a delay in a Kotlin coroutine made in the production code tested.
Implement
In this case SomeViewModel's view state is being tested. In the ERROR state a view state is emitted with the error value being true. After the defined Snackbar time length has passed using a delay a new view state is emitted with the error value set to false.
SomeViewModel.kt
private fun loadNetwork() {
repository.getData(...).onEach {
when (it.status) {
LOADING -> ...
SUCCESS ...
ERROR -> {
_viewState.value = FeedViewState.SomeFeedViewState(
isLoading = false,
feed = it.data,
isError = true
)
delay(SNACKBAR_LENGTH)
_viewState.value = FeedViewState.SomeFeedViewState(
isLoading = false,
feed = it.data,
isError = false
)
}
}
}.launchIn(coroutineScope)
}
There are numerous ways to handle the delay. advanceUntilIdle is good because it doesn't require specifying a hardcoded length. Also, if injecting the TestCoroutineDispatcher, as outlined by Craig Russell, this will be handled by the same dispatcher used inside of the ViewModel.
SomeTest.kt
private val testDispatcher = TestCoroutineDispatcher()
private val testScope = TestCoroutineScope(testDispatcher)
// Code that initiates the ViewModel emission of the view state(s) here.
testDispatcher.advanceUntilIdle()
These will also work:
testScope.advanceUntilIdle()
testDispatcher.delay(SNACKBAR_LENGTH)
delay(SNACKBAR_LENGTH)
testDispatcher.resumeDispatcher()
testScope.resumeDispatcher()
testDispatcher.advanceTimeBy(SNACKBAR_LENGTH)
testScope.advanceTimeBy(SNACKBAR_LENGTH)
Error without handling the delay
kotlinx.coroutines.test.UncompletedCoroutinesError: Unfinished coroutines during teardown. Ensure all coroutines are completed or cancelled by your test.
at kotlinx.coroutines.test.TestCoroutineDispatcher.cleanupTestCoroutines(TestCoroutineDispatcher.kt:178)
at app.topcafes.FeedTest.cleanUpTest(FeedTest.kt:127)
at app.topcafes.FeedTest.access$cleanUpTest(FeedTest.kt:28)
at app.topcafes.FeedTest$topCafesTest$1.invokeSuspend(FeedTest.kt:106)
at app.topcafes.FeedTest$topCafesTest$1.invoke(FeedTest.kt)
at kotlinx.coroutines.test.TestBuildersKt$runBlockingTest$deferred$1.invokeSuspend(TestBuilders.kt:50)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
at kotlinx.coroutines.test.TestCoroutineDispatcher.dispatch(TestCoroutineDispatcher.kt:50)
at kotlinx.coroutines.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:288)
at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26)
at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109)
at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:158)
at kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt:91)
at kotlinx.coroutines.BuildersKt.async(Unknown Source)
at kotlinx.coroutines.BuildersKt__Builders_commonKt.async$default(Builders.common.kt:84)
at kotlinx.coroutines.BuildersKt.async$default(Unknown Source)
at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:49)
at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:80)
at app.topcafes.FeedTest.topCafesTest(FeedTest.kt:41)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
In kotlinx.coroutines v0.23.0 they introduced a TestCoroutineContext.
Pro: it makes truly testing coroutines with delay possible. You can set the CoroutineContext's virtual clock to a moment in time and verify the expected behavior.
Con: if your coroutine code doesn't use delay, and you just want it to execute synchronously on the calling thread, it is slightly more cumbersome to use than the TestUiContext from #bj0's answer (you need to call triggerActions() on the TestCoroutineContext to get the coroutine to execute).
Sidenote:
The TestCoroutineContext now lives in the kotlinx-coroutines-test module starting with coroutines version 1.2.1, and will be marked deprecated or not exist in the standard coroutine library in versions above this version.

kotlin and ArgumentCaptor - IllegalStateException

I have a problem with capturing the Class argument via ArgumentCaptor. My test class looks like this:
#RunWith(RobolectricGradleTestRunner::class)
#Config(sdk = intArrayOf(21), constants = BuildConfig::class)
class MyViewModelTest {
#Mock
lateinit var activityHandlerMock: IActivityHandler;
#Captor
lateinit var classCaptor: ArgumentCaptor<Class<BaseActivity>>
#Captor
lateinit var booleanCaptor: ArgumentCaptor<Boolean>
private var objectUnderTest: MyViewModel? = null
#Before
fun setUp() {
initMocks(this)
...
objectUnderTest = MyViewModel(...)
}
#Test
fun thatNavigatesToAddListScreenOnAddClicked(){
//given
//when
objectUnderTest?.addNewList()
//then
verify(activityHandlerMock).navigateTo(classCaptor.capture(), booleanCaptor.capture())
var clazz = classCaptor.value
assertNotNull(clazz);
assertFalse(booleanCaptor.value);
}
}
When I run the test, following exception is thrown:
java.lang.IllegalStateException: classCaptor.capture() must not be null
Is it possible to use argument captors in kotlin?
=========
UPDATE 1:
Kotlin: 1.0.0-beta-4584
Mockito: 1.10.19
Robolectric: 3.0
=========
UPDATE 2:
Stacktrace:
java.lang.IllegalStateException: classCaptor.capture() must not be null
at com.example.view.model.ShoplistsViewModelTest.thatNavigatesToAddListScreenOnAddClicked(ShoplistsViewModelTest.kt:92)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at org.robolectric.RobolectricTestRunner$2.evaluate(RobolectricTestRunner.java:251)
at org.robolectric.RobolectricTestRunner.runChild(RobolectricTestRunner.java:188)
at org.robolectric.RobolectricTestRunner.runChild(RobolectricTestRunner.java:54)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.robolectric.RobolectricTestRunner$1.evaluate(RobolectricTestRunner.java:152)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:234)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:74)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
From this blog
"Getting matchers to work with Kotlin can be a problem. If you have a method written in kotlin that does not take a nullable parameter then we cannot match with it using Mockito.any(). This is because it can return void and this is not assignable to a non-nullable parameter. If the method being matched is written in Java then I think that it will work as all Java objects are implicitly nullable."
A wrapper function is needed that returns ArgumentCaptor.capture() as nullable type.
Add the following as a helper method to your test
fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
Please see, MockitoKotlinHelpers.kt provided by Google in the Android Architecture repo for reference. the capture function provides a convenient way to call ArgumentCaptor.capture(). Call
verify(activityHandlerMock).navigateTo(capture(classCaptor), capture(booleanCaptor))
Update: If the above solution does not work for you, please check Roberto Leinardi's solution in the comments below.
The return value of classCaptor.capture() is null, but the signature of IActivityHandler#navigateTo(Class, Boolean) does not allow a null argument.
The mockito-kotlin library provides supporting functions to solve this problem.
Code should be:
#Captor
lateinit var classCaptor: ArgumentCaptor<Class<BaseActivity>>
#Captor
lateinit var booleanCaptor: ArgumentCaptor<Boolean>
...
#Test
fun thatNavigatesToAddListScreenOnAddClicked(){
//given
//when
objectUnderTest?.addNewList()
//then
verify(activityHandlerMock).navigateTo(
com.nhaarman.mockitokotlin2.capture<Class<BaseActivity>>(classCaptor.capture()),
com.nhaarman.mockitokotlin2.capture<Boolean>(booleanCaptor.capture())
)
var clazzValue = classCaptor.value
assertNotNull(clazzValue);
val booleanValue = booleanCaptor.value
assertFalse(booleanValue);
}
OR
var classCaptor = com.nhaarman.mockitokotlin2.argumentCaptor<Class<BaseActivity>>()
var booleanCaptor = com.nhaarman.mockitokotlin2.argumentCaptor<Boolean>()
...
verify(activityHandlerMock).navigateTo(
classCaptor.capture(),
booleanCaptor.capture()
)
also in build.gradle add this:
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
Use kotlin-mockito https://mvnrepository.com/artifact/com.nhaarman/mockito-kotlin/1.5.0 as dependency and sample code as written below :
argumentCaptor<Hotel>().apply {
verify(hotelSaveService).save(capture())
assertThat(allValues.size).isEqualTo(1)
assertThat(firstValue.name).isEqualTo("İstanbul Hotel")
assertThat(firstValue.totalRoomCount).isEqualTo(10000L)
assertThat(firstValue.freeRoomCount).isEqualTo(5000L)
}
As stated by CoolMind in the comment, you first need to add gradle import for Kotlin-Mockito and then shift all your imports to use this library. Your imports will now look like:
import com.nhaarman.mockitokotlin2.argumentCaptor
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.eq
import com.nhaarman.mockitokotlin2.isNull
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify
Then your test class will be something like this:
val mArgumentCaptor = argumentCaptor<SignUpInteractor.Callback>()
#Test
fun signUp_success() {
val customer = Customer().apply {
name = "Test Name"
email = "test#example.com"
phone = "0123444456789"
phoneDdi = "+92"
phoneNumber = ""
countryCode = "92"
password = "123456"
}
mPresenter.signUp(customer)
verify(mView).showProgress()
verify(mInteractor).createAccount(any(), isNull(), mArgumentCaptor.capture())
}
According this solution my solution here:
fun <T> uninitialized(): T = null as T
//open verificator
val verificator = verify(activityHandlerMock)
//capture (would be same with all matchers)
classCaptor.capture()
booleanCaptor.capture()
//hack
verificator.navigateTo(uninitialized(), uninitialized())
Came here after the kotlin-Mockito library didn't help.
I created a solution using reflection.
It is a function which extracts the argument provided to the mocked-object earlier:
fun <T: Any, S> getTheArgOfUsedFunctionInMockObject(mockedObject: Any, function: (T) -> S, clsOfArgument: Class<T>): T{
val argCaptor= ArgumentCaptor.forClass(clsOfArgument)
val ver = verify(mockedObject)
argCaptor.capture()
ver.javaClass.methods.first { it.name == function.reflect()!!.name }.invoke(ver, uninitialized())
return argCaptor.value
}
private fun <T> uninitialized(): T = null as T
Usage:
(Say I have mocked my repository and tested a viewModel. After calling the viewModel's "update()" method with a MenuObject object, I want to make sure that the MenuObject actually called upon the repository's "updateMenuObject()" method:
viewModel.update(menuObjectToUpdate)
val arg = getTheArgOfUsedFunctionInMockObject(mockedRepo, mockedRepo::updateMenuObject, MenuObject::class.java)
assertEquals(menuObjectToUpdate, arg)
You can write a wrapper over argument captor
class CaptorWrapper<T:Any>(private val captor:ArgumentCaptor<T>, private val obj:T){
fun capture():T{
captor.capture()
return obj
}
fun captor():ArgumentCaptor<T>{
return captor
}
}
Another approach:
/**
* Use instead of ArgumentMatcher.argThat(matcher: ArgumentMatcher<T>)
*/
fun <T> safeArgThat(matcher: ArgumentMatcher<T>): T {
ThreadSafeMockingProgress.mockingProgress().argumentMatcherStorage
.reportMatcher(matcher)
return uninitialized()
}
#Suppress("UNCHECKED_CAST")
private fun <T> uninitialized(): T = null as T
Usage:
verify(spiedElement, times(1)).method(
safeArgThat(
CustomMatcher()
)
)
If none of the fine solutions presented worked for you, here is one more way to try. It's based on Mockito-Kotlin.
[app/build.gradle]
dependencies {
...
testImplementation 'org.mockito.kotlin:mockito-kotlin:3.2.0'
}
Define Rule and Mock in your test file.
#RunWith(AndroidJUnit4::class)
class MockitoTest {
#get:Rule
val mockitoRule: MockitoRule = MockitoJUnit.rule()
#Mock
private lateinit var mockList: MutableList<String>
And here is an example.
#Test
fun `argument captor`() {
mockList.add("one")
mockList.add("two")
argumentCaptor<String>().apply {
// Verify that "add()" is called twice, and capture the arguments.
verify(mockList, times(2)).add(capture())
assertEquals(2, allValues.size)
assertEquals("one", firstValue)
assertEquals("two", secondValue)
}
}
}
Alternatively, you can also use #Captor as well.
#Captor
private lateinit var argumentCaptor: ArgumentCaptor<String>
#Test
fun `argument captor`() {
mockList.add("one")
mockList.add("two")
verify(mockList, times(2)).add(capture(argumentCaptor))
assertEquals(2, argumentCaptor.allValues.size)
assertEquals("one", argumentCaptor.firstValue)
assertEquals("two", argumentCaptor.secondValue)
}

Categories

Resources