Lately we’ve been wrapping callback into Coroutine...
# android
p
Lately we’ve been wrapping callback into Coroutines for libraries that specifically need callbacks to function and offer no Coroutines nor Rx connectors. The snippet we’re using to achieve that comes from kotlinlang forums, some of you may recognize it.
Copy code
suspend fun <T> awaitCallback (block: (Callback<T>) -> Unit) : T =
    suspendCancellableCoroutine { continuation ->
        block(object : Callback<T> {
            override fun onComplete(result: T) {
                if (continuation.isActive) {
                    continuation.resume(result)
                } else {
                    ...
                }
            }
        })
    }
I admit I’m not fully mastering that snippet (could be arguable to use it in our codebase) but it’s allowing us to get rid of callbacks when there’s no alternatives. We’re using it with firebase iirc. It’s working well, however we have a lot of trouble covering that particular snippet of code using Unit Tests. We’re using Mockito/Powermockito and seting up the unit test with this code :
Copy code
@Captor
private lateinit var callbackCaptor: ArgumentCaptor<Callback<Any>>

@Mock
private lateinit var lambdaReceiver: (Callback<Any>) -> Unit
Anybody do have tips to cover that the above method using Mockito/Powermockito ?
e
You already have the right setup. //the syntax might be wrong here, have been a while since I used mockito Calling:`awaitCallback(lambdaReceiver)` Lets you do:
verify(lambdaReceiver.invoke(callbackCaptor))
after this line you have the callback in callbackCaptor You can call the callback with diferent values at this point. The problem is verify the return of you method after the invocation of the callback. You can launch a coroutine and the the first call in it and the verification after it. It should look something like this:
Copy code
launch {
    val result = awaitCallback(lambdaReceiver)
    //do your assertions for example
    assertEquals(expected, result)
}
verify(lambdaReceiver.invoke(callbackCaptor))
callbackCaptor.value.oncomplete(expected)
I am not able to run this right now, you might need to save the job resulting from launch and
.join()
after the
oncomplete
is called
p
Yea usually I have no issues with Unit Tests, I always been quite the partisan of Unit Test since years but this one got me headache. I’ll try that
e
for unit testing you might run all this inside a
runBlocking
so the java process waits for the completion of the coroutine
p
Yep that’s what I’m doing but I’ve ran into somes issues : - either it doesn’t complete like you said above - I tried to tinker with the lambda by mocking it to force invoke but the argument of my lambda seems wrong at execution
Cause yea the lambda above is just a mock I’m not sure if I have to force it to trigger, like a regular mock, by invoking it. Using regular mock when(lambdaReceiver.invoke(any()).invoke... Then I can’t manage to capture the invoke so I tried to mock it using thenAnswer and even with this liek I said above the arguments seems wrong.
Like this (using BBDMockito sorry lol)
Copy code
given(lambdaReceiver.invoke(any())).willAnswer {
                val argument = it.getArgument<(Callback<Any>) -> Unit>(0)
                val completion = argument as (Callback<Any>) -> Unit
                completion.invoke(callbackCaptor.capture())

            }
e
It's possible that it is not completing because it is running the
verify
before the code inside the
launch
is called
p
I assumed it was because the continuation needed a result from the callback before being resumed with the result. That was why I went with the argumentcaptor to trigger it manually. Sadly it’s not that easy it seems
e
After some trying it seems like something like this might work:
Copy code
launch {
    val result = awaitCallback(lambdaReceiver)
    //do your assertions for example
    assertEquals(expected, result)
}
launch {
    verify(lambdaReceiver.invoke(callbackCaptor))
   callbackCaptor.value.oncomplete(expected)
}
p
For some reason runBlocking seems to fail where Globalscope.launch works
I’m using only one block and got success too
e
the call to
awaitCallback
should run in diferent coroutine than the rest so it can suspend while it waits for the function to return. With my previous example it didn't work because it was calling verify before any code in the
launch
was called, wrapping the
verify
and `onComplete`in another
launch
allows the suspend function to be called and suspend
if you are soing your asertions in the
GlobalScope.launch
it is possible that the code is never actually run
p
Yea you’re right
I’ve ran it with coverage it doesn’t
e
without a runBlocking (or a Thread.sleep or something) the process finishes
p
For some reason with breakpoint and the two GS.launch it doesn’t seem to stop in my code. Weird.
e
with runblocking and two launch blocks it works for me (using mockk)
p
I was doing the same thing lol (glad to know we’re following the same mind process lol) test pass indeed but with breakpoint still issues (doesn’t pass through) and coverage show the inner block is not covered
Still using mockito. I’ve seen advice to use mockk on stackoverflow also.
Might behave better with coroutines...
e
Copy code
@Test
    fun testAwaitCallback() {
        val mockedBlock: (Callback<Int>) -> Unit = mockk(relaxed = true)
        val slot = slot<Callback<Int>>()
        runBlocking {
            launch {
                val result = awaitCallback(mockedBlock)
                assertEquals(5, result)
            }
            launch {
                verify {
                    mockedBlock.invoke(capture(slot))
                }
                slot.captured.onComplete(5)
            }
        }
    }
this worked for me, if I use
fail
in either block it fails the test
p
Oh thx I'll try that
Just a headsup on this : it’s working with Mockito-Kotlin, I did break point and failing tests too and it works. But for some reason the Coverage is bugged (IDE converage didn’t tried with JaCoCo on Sonar yet.
Snippet of the test :
Copy code
@Test
    fun awaitCallback() {
        runBlocking {
            launch {
                val result = sut.awaitCallback(lambdaReceiver)
                assertThat(result).isEqualTo(callbackValue)
            }

            launch {
                then(lambdaReceiver).should().invoke(capture(callbackCaptor))
                callbackCaptor.value.onComplete(callbackValue)
            }
        }
    }
Thanks you again for your help on this.
Seems like Mockito is having a hard time with Kotlin when trying to capture callbacks in lambda when using coroutines
e
There is mockito kotlin: https://github.com/nhaarman/mockito-kotlin If you are not using it I recommend checking it out, might help with some things
p
Oh yea I imported it on the project and it’s working well.
I think at the end the problem might have been the .capture() that was not working with Kotlin (using mockito only). I might have forgotten to add the kotlin specific spnippet like I did with any() and all other mockito argument matchers
But wrapping two launch in a runblocking was mandatory so this thread actually helped me beside the argumentmatcher problem.
I was reluctant to add mockito-kotlin since I’m very picky with my libs but well mockito-kotlin is handy.