Coroutines: Overriding OKHttp’s dispatcher to use AsyncTasks’s ThreadPoolExecutor so Espresso can assert successfully

Issue

I am migrating an app that uses Retrofit to work with coroutines. The app has some UATs that are failing because Espresso does not wait for the coroutines to complete and asserts immediately.

The CoroutineCallAdapterFactory, by default, uses OkHttp’s Dispatcher to perform asynchronous requests, but Espresso only monitors the UI Thread and AsyncTaks‘s Thread pool. One solution I thought of is to force OkHttp‘s Dispatcher to use AsyncTask‘s ThreadPoolExecutor.

val dispatcher  Dispatcher(AsyncTask.THREAD_POOL_EXECUTOR as ExecutorService)
okHttpClientBuilder.dispatcher(dispatcher)

This seems to work and the tests pass.

Is this a bad idea? Is registering an IdlingResource still a better option?

Solution

Without more details of your setup, I’m not sure if this information will help but I’ll mention a few tips that may help with your tests:

Convert Executors

You can also go the other way and create a coroutine dispatcher from any executor using:

AsyncTask.THREAD_POOL_EXECUTOR.asCoroutineDispatcher()

And of course, this can be used anyplace you’d have something like Dispatchers.Main meaning you can create a scope from this and launch your coroutines from that scope and Espresso should monitor the underlying executor pool for completion. For example:

...
val espressoScope  CoroutineScope(AsyncTask.THREAD_POOL_EXECUTOR.asCoroutineDispatcher())
...
espressoScope.launch { api.getBooks() }

Similarly, you can do things like:

val asyncTaskContext  AsyncTask.THREAD_POOL_EXECUTOR.asCoroutineDispatcher()
withContext(asyncTaskContext) {
    api.getBooks()
}

// OR:

@Test
fun someAndroidTest()  runBlocking(asyncTaskContext) {
    // espresso logic
}

Join Jobs (recommended)

Last but not least, you can join any jobs that you create in your test and the test will wait until the job completes before exiting. This sounds like the approach that would help most in your situation since you really just want to wait until the coroutines are complete:

@Test
fun `first book has a title`()  runBlocking {
    launch {
        // run a function that suspends and takes a while
        val firstBook  api.getAllBooks().first()
        assertNotNull(firstBook.title)
    }.join()
}

Answered By – gMale

Leave a Comment