Retrofitting Espresso
How to get Espresso to wait for RxJava's background schedulers
Missing the target by ThisParticularGreg is licensed under CC BY-SA 2.0
Ask any developer what their top five libraries for Android development are, and there’s a good chance they include RxJava and Retrofit for making network calls. Dead Man’s Snitch utilizes both. As we developed Dead Man’s Snitch for Android, we learned how powerful a combination this is, and also how difficult it can be to test properly.
We use Espresso for instrumentation testing. Espresso provides an API for performing actions and assertions on your app’s UI. Using this API, Espresso is pretty good at knowing when it’s okay to perform the next operation without having to litter your tests with sleep()
to wait for the view to settle. But it also has a huge blindspot for background processes. So if you swipe to refresh your RecyclerView
, Espresso will move on to the next operation while you’re still waiting for a server response. You have to find some way of telling Espresso to wait for it to finish.
Let’s look at a simple example of loading content from a web service:
class ItemDetailActivity: Activity() {
override fun onResume() {
super.onResume()
refreshing = true
itemService.getItem(id = 42)
.subscribeOn(Schedulers.io())
.flatMap { result -> parseResponse(result) }
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ item ->
refreshing = false
title.text = item.title
body.text = item.body
},
{ throwable ->
refreshing = false
displayError(throwable)
}
)
}
}
When we resume our activity, we make a network call to our API on Schedulers.io()
using a Retrofit service returning an Observable<Result<Item>>
. Then we parse the response, switch to the main thread, and update the UI in subscribe
. The accompanying test might look something like this:
@Test fun startupLoadsContentFromNetwork() {
activityRule.startActivity()
onView(withId(R.id.title))
.check(matches(withText("The Meaning of Life, the Universe, and Everything")))
onView(withId(R.id.body))
.check(matches(withText("42")))
}
In this case, Espresso expects to see content as soon as the Activity launches; it has no idea that it needs to wait for the network call to finish. You could put a sleep()
in the test before the check, but guessing network response times will lead to sleepless nights and fragile tests. You could wrap a Scheduler
in an IdlingResource
using RxJavaHooks
, but that’s a lot of effort with endless ways to get it wrong. There is an easier way.
Espresso already waits for certain background processes, but since we use RxJava, we tend to avoid it: the dreaded AsyncTask
.
Espresso is aware of AsyncTask
s, and will wait for them to complete before proceeding. In RxJava, we can create a custom scheduler that uses the AsyncTask
thread pool executor; it will run in the background, but still show up on Espresso’s radar. Using RxJavaHooks
, we can override Schedulers.io()
to return our new Espresso-aware scheduler. Check it out:
class CustomTestRunner: AndroidJUnitRunner() {
override fun onStart() {
RxJavaHooks.setOnIOScheduler { Schedulers.from(AsyncTask.THREAD_POOL_EXECUTOR) }
super.onStart()
}
override fun onDestroy() {
super.onDestroy()
RxJavaHooks.reset()
}
}
This will override the default IO scheduler that gets returned from Schedulers.io()
when called. This means that Espresso can watch our network calls and wait until they finish, and our tests will pass without our intervention. You can finally delete those sleep()
calls that were keeping you up at night. Our test suite run time dropped by about 25% since we didn’t have to guess (and often overestimate) our service response time. We now have a fast, clean, and sleepless test suite.
One final thing to keep in mind: Espresso will wait for the background operation to finish before it will run any checks. If you need to test something while the process is running, you may need to find an alternative strategy.
Happy testing!
Comments
Missing instruction on adding custom TestRunner to the project
Yep, but you should avoid such mix of responsibilities in Activity class, so this is not “real life” example.