Espresso UI testing with RxJava

I’m happy to announce the release of RxPresso, a new library to make Espresso UI testing easy for Android applications using RxJava. It will help you set up and test the UI of your Android application, with a predictable data source.


What if we could do UI testing with a predictable data source that would be easy to setup? Here’s a quick tl;dr version of the process.

If you use RxJava in your application your UI tests are as simple as this:

rxPresso.given(mockedRepo.getUser("id"))  
       .withEventsFrom(Observable.just(new User("some name")))
       .expect(any(User.class))
       .thenOnView(withText("some name"))
       .perform(click());

Go and check it out at github.com/novoda/rxpresso.


So why a new library? Isn’t Espresso enough?

At Novoda we try to test all aspects of our applications. This means presentation layer testing as well.

In the past we would rely on end-to-end Espresso tests for our presentation layer. It’s good to have a few end-to-end tests to check user journeys. Ideally, though, we would be able to fully test the presentation layer, including all error states, empty states, variations of data models, edge cases, and so on.

Relying on a real data source to test these presentation layer scenarios can be difficult, if not impossible. This reliance on a real data source can also lead to tests that are unreliable, flaky, or difficult to maintain. This leads to less UI tests being written as no self-respecting developer wants to write tests that can’t be relied on.

On the other hand, Unit tests are usually quite easy to implement and maintain as, if written well, they act as stand-alone tests, with no reliance on external resources.

So what if we could do UI testing with a predictable data source that would be easy to setup?

There are a few existing attempts at this. We initially experimented with MockWebServer, a mock datasource implementation, but we found that the difficulty of setting up each test was far from the ease of mocking data in a Unit test.

So we decided to see whether we could build a framework that would let us achieve the simplicity of unit test syntax in presentation layer tests. RxPresso is the result of this effort.

Test & Synchronise

RxPresso is a simple way to mock data while testing and for synchronizing Espresso view actions with Rx pipelines.

RxPresso doesn’t replace Espresso it’s simply a layer on top of it to deal with mocking data and synchronization.

http://i.imgur.com/LMaSOqf.png

Here’s what that looks like in practise:

rxPresso.given(mockedRepo.getUser("id"))  
       .withEventsFrom(Observable.just(new User("some name")))
       .expect(any(User.class))
       .thenOnView(withText("some name"))
       .perform(click());

If we look at this example in more detail:

rxPresso.given(mockedRepo.getUser("id"))  
       .withEventsFrom(Observable.just(new User("some name")))

You can compare those two lines to the equivalent in a normal Unit test for synchronous repository.

when(mockedRepo.getUser("id")).thenReturn(new User("some name"))  

And the remaining lines:

    .expect(any(User.class))
       .thenOnView(withText("some name"))
       .perform(click());

This tells what event we are waiting for before performing the test (here any User since we inject it above). The thenOnView() call is what links us back to Espresso world. It acts as the onView() of Espresso and allows you to chain whatever Espresso test you want.

A look under the hood

RxPresso relies on two concepts. The first one is a tool we developed called RxMocks, that allows us to control the Observables. The second one is a custom IdlingResource, that synchronizes Espresso with the Observables.

RxMocks mocks the Rx pipelines and allows us to inject data into them. This provides the stable data source needed to make tests reliable. In order to achieve this, we use Java reflection with Proxy.newProxyInstance() to generate a mock of the Repository.

Here you can see an example of a Repository that could be mocked:

public interface DataRepository {  
        Observable<User> getUser(String id);
        Observable<Articles> getArticles();
}

This mock of the repository will hold a distinct Observable for each method/parameter combination. In the Repository above, this would result in one Observable for the call getUser("Bob"), another one for getUser("Alice"), and so on. These Observables can be injected with any data you need to fully test all cases your presentation layer might need to handle.

RxMocks also provides a way to observe the events passing through those pipelines without impacting the subscription chain. This ability to monitor the events going through those pipelines is what enables us to synchronize the state of the IdlingResource with the injection of the mocked data.

IdlingResource is an interface provided by Espresso that allows you to declare to Espresso a resource to wait for before running tests. If you ever played with it yourself you know that implementing it is usually a lot of boilerplate code. Our IdlingResource implementation tells Espresso to wait for the data to come through the pipelines. This lock state ensures that your Espresso ViewActions are executed only once the data has been delivered to the presentation layer.

To see more examples of usage or set it up in your project, look at the read me or check out the demo.

We are really excited about how this enables us to test our presentation layer in an easy and extensive way and we already have plans to improve on RxPresso’s functionality. We are eager to get community feedback and make RxPresso a tool that meets the needs of a wider audience.

So feel free to contact us, open an issue on the Github repo, or even submit a pull request.

About Novoda

We plan, design, and develop the world’s most desirable Android products. Our team’s expertise helps brands like Sony, Motorola, Tesco, Channel4, BBC, and News Corp build fully customized Android devices or simply make their mobile experiences the best on the market. Since 2008, our full in-house teams work from London, Liverpool, Berlin, Barcelona, and NYC.

Let’s get in contact