Using Mockk to mock private function that takes a lambda - android

I am trying to write a unit test for a implementation of an abstract class I wrote. The method I'm trying to mock takes a lambda as it's only parameter. I'm trying to capture this lambda, so I can invoke it and get the result.
This is the method I'm trying to mock:
protected fun update(block: suspend S.() -> S?): Unit
I am using an extension function in my tests like this:
suspend inline fun <reified T : Model<S>, S : State> T.blah(
state: S,
block: (T) -> Unit
): S? {
val spy = spyk(this, recordPrivateCalls = true)
val slot = slot<suspend S.() -> S?>()
every { spy["update"](capture(slot)) } answers { Unit }
block(spy)
return slot.captured.invoke(state)
}
So I am creating a spy, then a slot, then when the update function is called, capture it so that it blocks the actual class from performing the call. Then I invoke the lambda myself and return the value.
However I keep getting this error:
io.mockk.MockKException: can't find function update(kotlin.jvm.functions.Function2$Subclass1#6bfa228c) for dynamic call
at io.mockk.InternalPlatformDsl.dynamicCall(InternalPlatformDsl.kt:122)
at io.mockk.MockKMatcherScope$DynamicCall.invoke(API.kt:1969)
I followed the stacktrace and set a breakpoint in the InternalPlatformDsl.kt class, and traced it to this block of code:
for ((idx, param) in it.parameters.withIndex()) {
val classifier = param.type.classifier
val matches = when (classifier) {
is KClass<*> -> classifier.isInstance(params[idx])
is KTypeParameter -> classifier.upperBounds.anyIsInstance(params[idx])
else -> false
}
if (!matches) {
return#firstOrNull false
}
}
It successfully matches the first parameter which is the class under test Model in this case, but it fails matching the second parameter because it is wrapped in the capture function.
Any ideas on how I can intercept this update call?
I'm using the latest version of mockk, and JUnit 4

Related

Android Kotlin - Unit test a delayed coroutine that invokes a lambda action when delay is finished

Ive been struggling with this for quite some time now, perhaps someone could help...
I have this function in my class under test:
fun launchForegroundTimer(context: Context) {
helper.log("AppRate", "[$TAG] Launching foreground count down [10 seconds]")
timerJob = helper.launchActionInMillisWithBundle(Dispatchers.Main, TimeUnit.SECOND.toMillis(10), context, this::showGoodPopupIfAllowed)
}
So in that function, I first write to some log and then I call a coroutine function that expects a Dispatcher param, how long to wait before running the action, Any object that I would like to pass on to the action and the actual action function that is invoked when time has passed.
So in this case, the this::showGoodPopupIfAllowed which is a private method in the class, gets called when the 10,000 ms have passed.
Here is that function:
private fun showGoodPopupIfAllowed(context: Context?) {
if (isAllowedToShowAppRate()) {
showGoodPopup(context)
}
}
In that first if, there are a bunch of checks that occur before I can call showGoodPopup(context)
Now, here is the helper.launchActionInMillisWithBundle function:
fun <T> launchActionInMillisWithBundle(dispatcher: CoroutineContext, inMillis: Long, bundle: T, action: (T) -> Unit): Job = CoroutineScope(dispatcher).launchInMillisWithBundle(inMillis, bundle, action)
And here is the actual CoroutineScope extension function:
fun <T> CoroutineScope.launchInMillisWithBundle(inMillisFromNow: Long, bundle: T, action: (T) -> Unit) = this.launch {
delay(inMillisFromNow)
action(bundle)
}
What I am trying to achieve is a UnitTest that calls the launchForegroundTimer function, calls the helper function with the appropriate arguments and also continue through and call that lambda showGoodPopupIfAllowed function where I can also provide mocked behaviour to all the IF statments that occur in isAllowedToShowAppRate.
Currently my test stops right after the launchActionInMillisWithBundle is called and the test just ends. I assume there is no real call to any coroutine because I am mocking the helper class... not sure how to continue here.
I read a few interesting articles but none seems to resolve such state.
My current test function looks like this:
private val appRaterManagerHelperMock = mockkClass(AppRaterManagerHelper::class)
private val timerJobMock = mockkClass(Job::class)
private val contextMock = mockkClass(Context::class)
#Test
fun `launch foreground timer`() {
every { appRaterManagerHelperMock.launchActionInMillisWithBundle(Dispatchers.Main, TimeUnit.SECOND.toMillis(10), contextMock, any()) } returns timerJobMock
val appRaterManager = AppRaterManager(appRaterManagerHelperMock)
appRaterManager.launchForegroundTimer(contextMock)
verify(exactly = 1) { appRaterManagerHelperMock.log("AppRate", "[AppRaterManager] Launching foreground count down [10 seconds]") }
}
I'm using mockk as my Mocking lib.
AppRaterManager is the class under test
I'd like to also mention that, in theory I could have moved the coroutine invocation outside the class under test. So an external class like activity.onResume() could launch some sort of countdown and then call directly a function that checks showGoodPopupIfAllowed(). But currently, please assume that I do not have any way to change the calling code so the timer and coroutine should remain in the class under test domain.
Thank you!
Alright, I read a bit deeper into capturing/answers over at https://mockk.io/#capturing and saw there is a capture function.
So I captured the lambda function in a slot which enables me invoke the lambda and then the actual code continues in the class under test. I can mock the rest of the behavior from there.
Here is my test function for this case (for anyone who gets stuck):
#Test
fun `launch foreground timer, not participating, not showing good popup`() {
val slot = slot<(Context) -> Unit>()
every { appRaterManagerHelperMock.launchActionInMillisWithBundle(Dispatchers.Main, TimeUnit.SECOND.toMillis(10), contextMock, capture(slot)) } answers {
slot.captured.invoke(contextMock)
timerJobMock
}
every { appRaterManagerHelperMock.isParticipating() } returns false
val appRaterManager = AppRaterManager(appRaterManagerHelperMock)
appRaterManager.launchForegroundTimer(contextMock)
verify(exactly = 1) { appRaterManagerHelperMock.log("AppRate", "[AppRaterManager] Launching foreground count down [10 seconds]") }
verify(exactly = 1) { appRaterManagerHelperMock.isParticipating() }
verify(exactly = 0) { appRaterManagerHelperMock.showGoodPopup(contextMock, appRaterManager) }
}
So what's left now is how to test the coroutine actually invokes the lambda after the provided delay time is up.

How to unit test suspend function with callback

I have the following code:
suspend fun initialize(sdk: Sdk) =
suspendCoroutine<Unit> { continuation ->
try {
sdk.initialize(
callback = { continuation.resume(Unit) },
onFailure = { error -> continuation.resumeWithException(SdkException(error.message)) })
} catch (exception: Exception) {
continuation.resumeWithException(
SdkException("Crash inside SDK", exception)
)
}
}
The Sdk is a third-party library. I'm using suspendCoroutine to suspend the coroutine and resume when the sdk finishes initializing.
Everything works fine but when I try to write a unit test like this I get the following IllegalStateException: This job has not completed yet. I'm using mockito-kotlin to write the following test:
#Test
fun `should initialize sdk correctly`() = runBlockingTest {
val sdk = mock<Sdk>()
initializeSdk(sdk)
verify(sdk).initialize(any(), any())
}
Basically what I want to do is to be able to test the resume and resumeWithException
Personally I'm not a fan of mocks. If Sdk is an interface, you could just provide your own test implementation to perform your tests (for success and error results). You can control exactly when/if the callback is called etc.
If Sdk is a class that you can't control, you could create an interface to abstract the Sdk class away, and make your initialize method use your own interface instead. However I have to admit this is not ideal.
If you stick with mocking, usually mocking libraries have a way for you to use invocation arguments to mock responses to method calls. With Mockito, it should be something like:
val sdk = mock<Sdk> {
on { initialize(any(), any()) } doAnswer { invocation ->
#Suppress("UNCHECKED_CAST")
val successCallback = invocation.arguments[0] as (() -> Unit) // use callback's function type here
successCallback.invoke()
}
}
(although I'm not an expert in Mockito, so there may be more concise or type-safe ways :D)

Unsure how to unit test interface in Kotlin with chained block

I'm working with Kotlin (for Android), along with jUnit and Koin.
I have an interface to consume that looks like this:
interface MyInterface {
fun vehicle(id: String, block: VehicleInterface.() -> Unit)
}
interface VehicleInterface {
fun requestVersion()
}
The code that calls this method looks like this:
import com.domain.protocol.thelibrary.sdk.MyInterface
// The below is injected via Koin
class SomeClass(private val myInterface: MyInterface) {
fun functionIWantToUnitTest(id: String) {
myInterface.vehicle(id) {
requestVersion()
}
}
}
I would like to unit test that when I call functionIWantToUnitTest and pass in the id "1" then myInterface.vehicle is called with id "1" and I'd also like to verify that requestVersion is then called.
I wrote a unit test as such:
#Test
fun `Ensure functionIWantToUnitTest calls requestVersion with the correct id`() {
every { myInterface.vehicle("1", any()) } returns Unit
someClass.functionIWantToUnitTest("1")
verifySequence {
myInterface.vehicle("1") {
requestVersion()
}
}
}
This fails as such:
java.lang.AssertionError: Verification failed: call 1 of 1:
MyInterface(myInterface#1).vehicle(eq(1), eq(lambda {}))). Only one matching call to
MyInterface(myInterface#1)/vehicle(String, Function1) happened, but arguments are not
matching:
[0]: argument: 1, matcher: eq(1), result: +
[1]: argument: lambda {}, matcher: eq(lambda {}), result: -
Can anyone please help me to understand what I'm doing wrong...
Any help gratefully accepted.

Using capture and mocks to unit test a class

I am trying to unit test the following class:
class UserProfileDetailsAnalyticUseCaseImp #Inject constructor(private val analyticsProvider: AnalyticsProvider) : UserProfileDetailsAnalyticUseCase {
override fun execute(cdsCustomer: CDSCustomer) {
with(analyticsProvider) {
log(AnalyticEvent.UserId(cdsCustomer.id.toString()))
log(AnalyticEvent.UserEmail(cdsCustomer.email))
}
}
}
And this is my unit test:
class UserProfileDetailsAnalyticUseCaseImpTest {
private lateinit var userProfileDetailsAnalyticUseCaseImp: UserProfileDetailsAnalyticUseCaseImp
private val analyticsProviders: AnalyticsProvider = mock()
#Before
fun setUp() {
userProfileDetailsAnalyticUseCaseImp = UserProfileDetailsAnalyticUseCaseImp(analyticsProviders)
}
#Test
fun `should send analytic event`() {
// Arrange
val cdsCustomer = CDSCustomer(
id = Random.nextInt(0, 100000),
email = UUID.randomUUID().toString())
val userIdCapture= argumentCaptor<AnalyticEvent.UserId>()
val userEmailCapture= argumentCaptor<AnalyticEvent.UserEmail>()
// Act
userProfileDetailsAnalyticUseCaseImp.execute(cdsCustomer)
// Assert
verify(analyticsProviders, atLeastOnce()).log(userIdCapture.capture())
verify(analyticsProviders, atLeastOnce()).log(userEmailCapture.capture())
assertThat(userIdCapture.firstValue.userId).isEqualTo(cdsCustomer.id.toString())
assertThat(userEmailCapture.firstValue.email).isEqualTo(cdsCustomer.email)
}
}
The error I get is the following:
AnalyticEvent$UserId cannot be cast to AnalyticEvent$UserEmail
I am suspecting that because class under test is creating a new object for each log method they will not be the same for the verified methods in the unit test
i.e log(AnalyticEvent.UserId(cdsCustomer.id.toString()))
As a new AnaltyicEvent.UserId will be created and just for the same AnalyticProvider mock
Many thanks for any suggetions
In the documentation of ArgumentCaptor we can read that:
This utility class doesn't do any type checks. The generic
signatures are only there to avoid casting in your code.
Moreover CapturingMatcher which is used for collecting captured arguments has a method which matches all objects:
public boolean matches(Object argument) {
return true;
}
It means that it is normal behaviour and even when we specify concrete type of captor it will record all arguments passed.
Of course all these arguments have to inherit from the same base class because in other case capture method will cause compilation error.
So, both your captors record two arguments.
To fix class cast exception for your test you can assert secondValue for email.
assertThat(userEmailCapture.secondValue.email).isEqualTo(cdsCustomer.email)
You can also stop using argument captors and simply verify invocations of log method.
verify(analyticsProviders).log(AnalyticEvent.UserId(cdsCustomer.id.toString()))
verify(analyticsProviders).log(AnalyticEvent.UserEmail(cdsCustomer.email))

Kotlin coroutines using produces and mockito to mock the producing job

I am testing Kotlin coroutines in my Android app and I am trying to do the following unit test
#Test fun `When getVenues success calls explore venues net controller and forwards result to listener`() =
runBlocking {
val near = "Barcelona"
val result = buildMockVenues()
val producerJob = produce<List<VenueModel>>(coroutineContext) { result.value }
whenever(venuesRepository.getVenues(eq(near))) doReturn producerJob // produce corooutine called inside interactor.getVenues(..)
interactor.getVenues(near, success, error) // call to real method
verify(venuesRepository).getVenues(eq(near))
verify(success).invoke(argThat {
value == result.value
})
}
The interactor method is as follows
fun getVenues(near: String, success: Callback<GetVenuesResult>,
error: Callback<GetVenuesResult>) =
postExecute {
repository.getVenues(near).consumeEach { venues ->
if (venues.isEmpty()) {
error(GetVenuesResult(venues, Throwable("No venues where found")))
} else {
success(GetVenuesResult(venues))
}
}
}
postExecute{..} is a method on a BaseInteractor that executes the function in the ui thread through a custom Executor that uses the launch(UI) coroutine from kotlin android coroutines library
fun <T> postExecute(uiFun: suspend () -> T) =
executor.ui(uiFun)
Then the repository.getVenues(..) function is also a coroutine that returns the ProducerJob using produce(CommonPool) {}
The problem is that it seams that success callback in the interactor function doesn't seem to be executed as per the
verify(success).invoke(argThat {
value == result.value
})
However, I do see while debugging that the execution in the interactor function reaches to the if (venues.isEmpty()) line inside the consumeEach but then from there exits and continues with the test, obviously failing on the verify for the success callback.
I am a bit new on coroutines so any help would be appreciated.
I figured this one out. I saw that the problem was just with this producing coroutine and not with the others tests that are also using coroutines and working just fine. I noticed that I actually missed the send on the mocked ProducingJob in order to have it actually produce a value, in this case the list of mocks. I just added that changing the mock of the producing job to
val producerJob = produce { send(result.value) }

Categories

Resources