I need to unit test concurrent calls ```runBlocki...
# coroutines
d
I need to unit test concurrent calls
Copy code
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
Copy code
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
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
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
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
this is an unit test, it is supposed to simulate the real deal.
Copy code
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
Copy code
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?
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.
Copy code
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
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
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