Consider following scenario
Function to test:
fun execute(appConfig: AppConfig): Deferred<String> = async {
registerDevice(appConfig.serviceId)
}
private suspend fun registerDevice(serviceId: String): String = suspendCoroutine {
//some nasty 3rd party library delivering data in a callback
lib.registerDeviceForService(object: Callback {
override fun onSuccess(response) {
it.resume(response.data)
}
}
)
}
Here is the test:
private val callbackCaptor = argumentCaptor<Callback<Response>>()
@Test
fun `ok result of registration results in success`() = blocking {
val deferred = classToTest.execute(appConfig)
mockRegisterResponseSuccess()
val result = deferred.await()
result.should.be.equal(trackingId)
}
private fun mockRegisterResponseSuccess(result: ApiResponseStatus) {
then(lib).should().registerDeviceForService(callbackCaptor.capture())
callbackCaptor.firstValue.onSuccess(Response(data = "testdata"))
}
suspendCoroutine blocks the call and code never gets to the mockRegisterReponseSuccess call to mock and capture the callback
execute returns Deferred<String> or String? Because these snippets are inconsistent: execute(appConfig: AppConfig): String, but val deferred = classToTest.execute(appConfig); ... deferred.await().
If it returns Deferred, then there shouldn't be any problems (just mock response before calling await).
If not:
suspendCoroutineblocks the call and code never gets to the mockRegisterReponseSuccess call to mock and capture the callback
That's more or less self-explanatory. You should mock response-related class before suspending current thread.
For example, consider following piece of blocking code:
val future: CompletableFuture<> = foo()
future.get()
mockServiceResponsibleForFutureCompletion()
This toy example will block forever, but it's not because CompletableFuture is untestable :)
Please provide self-contained example of the problem and behaviour you'd want to have.
@qwwdfsad updated to Deferred.
I actually tried both with async and with just suspend 鈥撀燽oth cases do not work
@qwwdfsad To make this test run successfully I need to capture the value passed into lib.registerDeviceForService and manually execute it's onSuccess() method
With Mockito it only works if you do:
verify(lib).registerDeviceForService(callbackCaptor.capture())
So only after the call to the test method was made, mocks counted all the execution paths and it's possible to capture some value
Basically, the real issue isn't with testing.
Real issue is that suspendCoroutine is the only way to work with callbacks, and it's blocking
With RxJava same code looks like
val testObserver = classToTest.execute().test()
mockRegisterResponseSuccess(ApiResponseStatus.OK)
mockInitAppPurchasesSuccess()
testObserver.assertValue(trackingId)
and it works fine
We need similar test tooling for coroutines
Sorry, you're providing too much your project-specific details, which I'm not aware of due to the lack of the context.
I'm happy to help you if you will provide a self-contained example of the problem: either a sample project or a single snippet which I can copy-paste into IDE and run (+include some dependencies like mockito if it's required). Without it, I can only speculate that it's some problem in mocking.
Please provide a reproducer with Deferred.
Here is a more generic example:
import kotlinx.coroutines.experimental.Deferred
import kotlinx.coroutines.experimental.async
import kotlin.coroutines.experimental.suspendCoroutine
interface LibCallback {
fun onResultReady(data: String)
}
class Lib {
fun doStuff(callback: LibCallback) {
callback.onResultReady("result ready")
}
}
class TestClass(private val lib: Lib) {
fun execute(): Deferred<String> = async {
callSuspendedCallback()
}
private suspend fun callSuspendedCallback(): String = suspendCoroutine {
lib.doStuff(object: LibCallback {
override fun onResultReady(data: String) {
it.resume(data)
}
})
}
}
Task is to test that calling testClassInstance.execute() returns back "result ready"
Test that fails.
Libraries used: Mockito, Mockito-kotlin, expekt
import kotlinx.coroutines.experimental.runBlocking
import org.junit.Test
fun <T> blocking(block: suspend () -> T) {
runBlocking { block() }
}
class TestClassTest {
private val lib: Lib = mock()
private val classToTest = TestClass()
val libCallbackCaptor = argumentCaptor<LibCallback>()
@Test
fun `executes and gets result`() = blocking {
val result = classToTest.execute().await()
then(lib).should().doStuff(libCallbackCaptor.capture())
libCallbackCaptor.firstValue.onResultReady("result ready")
result.should.be.equal("result ready")
}
}
That basically means that it's not possible to use Mockito ArgumentCaptor approach with suspendCoroutine
@Test
fun `executes and gets result`() = blocking {
val result = classToTest.execute().await()
then(lib).should().doStuff(libCallbackCaptor.capture())
libCallbackCaptor.firstValue.onResultReady("result ready")
result.should.be.equal("result ready")
}
Why are you expecting this test to work? You call await (suspending the whole coroutine) and then when deferred is complete, you are mocking the service which is supposed to complete deferred.
That's exactly the case I've described above with CompletableFuture. You should first mock the service and only then call await on it.
About mocking Kotlin code -- it's not a kotlinx.coroutines issue, just use a Kotlin-mockito.
Task is to test that calling testClassInstance.execute() returns back "result ready"
Something like this modulo proper mocking
@Test
fun sampleTest() = runTest {
val lib = mock(Lib::class.java)
val testInstance = TestClass(lib)
Mockito.`when`(lib.doStuff(ArgumentMatchers.any(LibCallback::class.java))).thenAnswer { invocation -> (invocation.getArgument(0) as LibCallback).onResultReady("result ready") }
val deferred = testInstance.execute()
assertEquals("result ready", deferred.await())
}
Thanks
I also found a workaround just now:
given(lib.doStuff(argThat {
it.onResultReady("result ready")
true
}).willAnswer {}
Version with argument captors worked nice with RxJava and testsubscribers
for posterity: using MockK you can write something like this
val lib: Lib = mockk()
val slot = slot
every {
lib.doStuff(capture(slot))
} answers {
slot.captured.onResultReady("result ready")
}
Most helpful comment
Why are you expecting this test to work? You call
await(suspending the whole coroutine) and then whendeferredis complete, you are mocking the service which is supposed to complete deferred.That's exactly the case I've described above with
CompletableFuture. You should first mock the service and only then callawaiton it.About mocking Kotlin code -- it's not a
kotlinx.coroutinesissue, just use a Kotlin-mockito.Something like this modulo proper mocking