(A first draft of this blog post originally appeared on my personal blog: Wait for It…a Deep Dive in Espresso's Idling Resources)
One of the challenges developers have to face when writing UI tests is waiting for asynchronous computations or I/O operations to be completed. In this post I'll describe how I solved that problem using the Espresso testing framework and a few gotchas I learned. I assume you're already familiar with Espresso, so I won't describe the philosophy behind it but instead I'll just focus on how to solve that problem the Espresso way.
Espresso introduces the concept of IdlingResource, which is a simple interface that:
Represents a resource of an application under test which can cause asynchronous background work to happen during test execution
The interface defines three methods:
- getName(): must return a non-null string that identifies an idling resource.
- isIdleNow(): returns the current idle state of the idling resource. If it returns true, the onTransitionToIdle() method on the registered ResourceCallback must have been previously called.
- registerIdleTransitionCallback(IdlingResource.ResourceCallback callback): normally this method is used to store a reference to the callback to notify it of a change in the idle state.
For example, an implementation for an idling resource that waits for a page to be fully loaded in a WebView will look something like this:
After creating your own custom idling resource, it needs to be registered with Espresso by calling Espresso.registerIdlingResource(webViewIdlingResource).
Register a component tied to an Activity instance
Because of idempotence, only the first idling resource registered will be remembered by Espesso and this is bad for at least two reasons:
- all test cases will rely on that idling resource, but its reference to the webview is not valid anymore because a new activity has been created
- the context of the first activity is being leaked
To solve these issues, we can introduce an idling resource that is tied to the activity lifecycle and that injects and clears the reference to a component when appropriate.
Now let's modify our WebViewIdlingResource class to implement this interface
The last step is to find a good place to inject and clear the webview reference, for this we can leverage the ActivityLifecycleCallback interface
It's not particularly concise and there's a lot of wiring involved but it does what we want.
Register for a component tied to an Application instance
Things are easier if you have to wait for a component tied to the application object, since it's around for the whole life of the app and typically you don't have to care about
registering/unregistering components. These days 90% of the apps out there rely on an API to get data and need to handle asychronous I/O. On Android there are a few ways to do that, everyone with its pros and cons, but in any case you need to wait for the API call to return. If
you're using AsyncTask Espresso has support for it out of the box, but there can be reasons why you'd want to not use the built-in Android components and use Java thread pools
instead. In this case you'd need to define a custom idling resource that checks if the executor(s) used by the application are idle. Looking at the Espresso source code, with a small refactoring
to the AsyncTaskPoolMonitor class (Espresso uses it to check if there is some tasks running on the AsyncTask thread pool executor) a general-purpose ThreadPoolIdlingResource can be implemented.
There are a few edge cases that one need to keep in mind in order to use idling resources properly. I described the current solution I ended up implementing, another - probably less error-prone - solution would be to have an Espresso.unregisterIdlingResource(myIdlingResource) API (there is already a feature request to add it). As for registering idling resources, I ended up doing it inside the callApplicationOnCreate(app) method of a custom InstrumentationTestRunner, this way I am sure the registration happens only once when the test suite is created.
P.S.: Jimdo is looking for an Android developer to join our team in Hamburg! Feel free to contact me if you're interested.