I am creating a library and I'm using Retrofit with a call-adapter that gives me a Deferred<> value.
In a function in my code I call launch {}, and inside that i try-catch the values, and possible exceptions - calling different callbacks for different results.
The resources I've found on testing coroutines are all about testing suspended functions, and runBlocking {} is the solution to everything. Except for me it isn't
I made a quick example
@Mock
val mockListener: DoSomething.Listener = mock()
@Test
fun testSomething() {
val doer = DoSomething(mockListener)
runBlocking {
doer.doIt()
verify(mockListener).listen(any())
}
}
class DoSomething(val listener: Listener) {
interface Listener {
fun listen(s: String)
}
fun doIt() {
launch {
listener.listen(theThing().await())
}
}
private fun theThing(): Deferred<String> {
return async {
delay(5, TimeUnit.SECONDS)
return@async "Wow, a thing"
}
}
}
What I want is for the actually run all functions. The test should take 5 seconds minimum, but it just runs through the code in a couple of millisconds- ie. it doesn't block.
I've tried adding
runBlocking {
launch {
// doer.doIt()
}.joinChildren()
}
And similar practices but I just can't get the test to actually wait for my launch inside of another class to finish before the test is finished.
Placing the verify(...) outside of the runBlocking also makes the test fail, which it should.
Any input, helpers, good practice etc. is appreciated!
You can provide the CoroutineContext explicitly for your doIt() function:
fun doIt(context: CoroutineContext = DefaultDispatcher) {
launch(context) {
listener.listen(theThing().await()
}
}
With this parameter you could easily change the coroutine context - in your test code you use the blocking context:
runBlocking {
doer.doIt(coroutineContext)
}
BTW: You don't need to use launch and async. With launch you are in a suspendable context and you don't need to run theThing() asynchronously. Especially if you invoke await() in the next step:
fun doIt(context: CoroutineContext = DefaultDispatcher) {
launch(context) {
listener.listen(theThing())
}
}
private suspend fun theThing(): String {
delay(5, TimeUnit.SECONDS)
return "Wow, a thing"
}
Best way would be not to swallow Job in your doIt() function as you do now.
Instead of
fun doIt() {
launch {
listener.listen(theThing().await())
}
}
Do
fun doIt() = launch {
listener.listen(theThing().await())
}
That way your function will return a coroutine, which you can wait for:
doIt().join()
Better still is to use async() instead of launch()
Another comment is that doIt() should be actually doItAsync(), as suggested by Kotlin guidelines.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With