Michael Evans

A bunch of technobabble.

Using Espresso for Easy UI Testing

| Comments

One thing that I notice when I talk to Android developers is that a lot of them don’t put an emphasis on testing. They say that it’s too hard to write them or that they are too hard to integrate and set up, or give a bunch of other reasons why they don’t. But it’s actually pretty simple to write Espresso tests, and they really aren’t that hard to integrate with your code base.

Easy to write

Espresso tests are dead simple to write. They come in three parts.

  1. ViewMatchers – something to find the view to act upon/assert something about
  2. ViewActions – something to perform an action (type text, click a button)
  3. ViewAssertions – something to verify what you expect

For example, the following test would type the name “Steve” into an EditText with the id name_field, click a Button with the id greet_button and then verify that the text “Hello Steve!” appears on the screen:

1
2
3
4
5
6
@Test
public void testSayHello() {
  onView(withId(R.id.name_field)).perform(typeText("Steve"));
  onView(withId(R.id.greet_button)).perform(click());
  onView(withText("Hello Steve!")).check(matches(isDisplayed()));
}

Seems simple enough right? But what about when other threads are involved?

Integration

From the Espresso documentation:

The centerpiece of Espresso is its ability to seamlessly synchronize all test operations with the application under test. By default, Espresso waits for UI events in the current message queue to process and default AsyncTasks* to complete before it moves on to the next test operation. This should address the majority of application/test synchronization in your application.”

But if you’re like me, you’re not writing AsyncTasks to handle your background operations. My go-to tool for making HTTP requests (probably one of the most common uses of AsyncTask) is Retrofit. So what can we do? Espresso has an API called registerIdlingResource, which allows you to synchronize your custom logic with Espresso.

With this knowledge, one way you might approach this is to implement a mock version of your Retrofit interface, and then use something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class MockApiService implements ApiService, IdlingResource {
  
  private final ApiService api;
  private final AtomicInteger counter;
  private final List<ResourceCallback> callbacks;

  public MockApiService(ApiService api) {
      this.api = api;
      this.callbacks = new ArrayList<>(); 
      this.counter = new AtomicInteger(0);
  }

  @Override
  public Response doWork() {
      counter.incrementAndGet();
      return decrementAndNotify(api.doWork());
  }

  @Override
  public String getName() {
      return this.getClass().getName();
  }

  @Override
  public boolean isIdleNow() {
      return counter.get() == 0;
  }

  @Override
  public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
      callbacks.add(resourceCallback);
  }

  private <T> T decrementAndNotify(T data) {
      counter.decrementAndGet();
      notifyIdle();
      return data;
  }

  private void notifyIdle() {
      if (counter.get() == 0) {
          for (ResourceCallback cb : callbacks) {
              cb.onTransitionToIdle();
          }
      }
  }

}

This tells Espresso that your app is idle after the methods are called. But you should immediately see the problem here – you’ll end up writing a TON of boilerplate. As you have more methods in your interface, and lot of repeated increment and decrement code…there must be a better way. (There is!)

The “trick” lies right in the selling point in the Espresso documentation, “Espresso waits for UI events… and default AsyncTasks to complete”. If we could somehow execute our Retrofit requests on the AsyncTasks’ ThreadPoolExecutor, we’d get sychronization for free!

Fortunately, Retrofit’s RestAdapter.Builder class has just such a method!

1
2
3
new RestAdapter.Builder()
   .setExecutors(AsyncTask.THREAD_POOL_EXECUTOR, new MainThreadExecutor())
   .build();

And it’s that simple – Now you have no excuse not to write some Espresso tests!

More Resources

Thanks to Huyen Tue Dao for editing this post!

Comments