https://kotlinlang.org logo
Title
d

Daniele Segato

08/04/2021, 7:59 AM
I need to unit test concurrent calls
runBlockingTest {
  val mutex = Mutex(true)
  val mockedApi = mock<MyApi> {
    onBlocking { invoke() } doSuspendableAnswer {
      mutex.withLock {
        // wait unlock to run
      }
    }
  }
  val sut = MyTestedClass(
    api = mockedApi,
  )

  println("async callA"}
  val callA = async { sut.doCall() }
  println("async callB"}
  val callB = async { sut.doCall() }

  mutex.unlock()
  // ....
}
this test never completes, it runs forever and stop at
async callA
the
async callB
is NEVER printed the
MyTestedClass
class MyTestedClass(private val api: MyApi) {
  fun doCall() { // this is NOT suspendable
    runBlocking {
      api() // but this is suspendable
    }
  }
}
Isn't async supposed to be non-blocking? I would expect callA and callB to be executed concurrently until they reach the mutex but apparently the first async is blocking in the test and stop at the mutex blocking all the test. What am I missing here?
m

mateusz.kwiecinski

08/04/2021, 8:20 AM
runBlocking
does not suspend but blocks the thread, which is never desired effect when using coroutines. (I totally recommend this article: https://elizarov.medium.com/blocking-threads-suspending-coroutines-d33e11bf4761) It is safe to say that you never want to use
runBlocking
. In your scenario you block the thread your test invokes on, hence the lack of completion. You can’t invoke i.e.
println("async callB")
because the test thread thread that supposed to do so is waits until
runBlocking
completes Most probably you want to define your
doCall
method as
suspend fun
d

Daniele Segato

08/04/2021, 8:22 AM
thanks, I know, but I need runBlocking there cause it's a method that needs to run inside an OkHttp interceptor, which doesn't support coroutines
m

mateusz.kwiecinski

08/04/2021, 8:30 AM
I’d suggest then to either move the runBlocking call to the interceptor class (as that’s the place which is invoked on a background thread and doesn’t support coroutines) or if you prefer keeping a blocking api then use blocking utilities in your test. Launch new threads, use latches and block, join and await them. You could wrap blocking code in coroutines, but that seems to miss the point of having a blocking api
d

Daniele Segato

08/04/2021, 8:40 AM
this is an unit test, it is supposed to simulate the real deal.
val threadPool = Executors.newFixedThreadPool(3)
            val dispatcher = threadPool.asCoroutineDispatcher()
try {
  // ...
  val callA = async(dispatcher) { sut.doCall() }
  val callB = async(dispatcher) { sut.doCall() }
  
  mutext.unlock()
  // ...
  joinAll(callA, callB)
} finally {
  dispatcer.close()
  threadPool.shutdownNow()
}
the external runBlockingTest gives me
java.lang.IllegalStateException: This job has not completed yet
looks like it is not waiting for those jobs to complete in joinAll
it is supposed to simulate the real deal.
what I mean by this is that the use of coroutine is an implementation detail, the interceptor shouldn't know about it
and using latches and wait seems to make the test way more complicated than the actual implementation
is there something wrong in the way I created the dispatcher and used it in the async?
Is there a documentation on how to test code using
runBlocking
?
oh right....
I see what you meant @mateusz.kwiecinski thanks. of course from outside the function is not a coroutine, so I shouldn't try to test it like it was.
val threadPool = Executors.newFixedThreadPool(3)

val callA = threadPool.submit(Runnable { sut.doCall() })
val callB = threadPool.submit(Runnable { sut.doCall() })

callA.get()
callB.get()

try {
} finally {
  threadPool.shutdownNow()
}
thanks
m

mateusz.kwiecinski

08/04/2021, 9:02 AM
Oh, great 😄 That was exactly my point ☝️ I really wasn’t sure why you wanted to test a regular thread-blocking code with coroutine test utilities and treat them like they were coroutines 😛 Buy yeah, that’s what I meant - if an blocking api is preferred I suggested to test in a “blocking way”, like the real consumer sees it (which is exactly what you said above 😛)
d

Daniele Segato

08/04/2021, 9:14 AM
yeah you were right of course. My bad for not picking up on what you meant right away
thank you very much for the help
😊 1