World-Class Testing Development Pipeline for Android - Part 4


In our previous blog post, “World-Class Testing Development Pipeline for Android - Part 3”, we talked about testing our integration with a server side API a.k.a., “the second part of our Testing Development Pipeline”. We discussed how to use a library like MockWebServer as an approach to test our code and reached the following conclusions:

  • - The server side API integration is one of the key points in application development.
  • - Testing mechanisms such as the authentication process and the data parsing is fundamental.
  • - We replaced production code implemented in the server side instead of replacing production code inside the API client.
  • - The tests created to verify our integration with the remote service also work as live documentation for our server side API.
  • - The scope for these tests are larger than usual.


In this blog post, we will review a testing approach that covers the third part of our Testing Development Pipeline under the theme “how to test our user application interface”.

Most of the mobile applications are based on strong UI components. These widgets or UI components are used to show information to the user. Without this component our application is basically a command line app. If we review most of the applications we’ve been working on, we arrive at the conclusion that almost all of them are only a front-end to display information obtained from a service. Most of the code written to develop these applications is UI code, code written on top of the Android SDK. That is why we consider the UI layer as important as the core components of our software and pay careful attention to effectively test our UI code.

Testing the application User Interface.

When dealing with an application user interface, we need to know if our code:

  • - Shows the correct information to the user once the UI is loaded.
  • - Shows the correct messages to the user as the result of a user interaction.
  • - Displays the correct screens given a user interaction.


In order to validate these three keys assumptions, we need to start a valid environment to execute our tests for which we can use an Android emulator or a device using Android as OS. Once the environment has been initialized, we should perform different actions in the application user interface as well as perform assertions over the information shown to the user. To test if the user interface is working as expected, we will perform assertions over the user interface components and perform simulation interactions.

To fully control the test scenario we are going to use test doubles -introduced in a previous blog post - exercising the subject under test. Accordingly, reducing the test scenario and improving the determinism of our tests will reduce their flakiness. Using test doubles we are going to control all the data our application can access, from the user logged into the system to the information shown to the user. One interesting point is that our tests, using mocked data, are not going to use the device internet connection at all.

To show the implementation we are going to use a Kata we have designed to practice UI testing. The little application implemented in this repository shows two Activities. The first one shows a list of superheroes and the second one shows detailed information of a superhero previously selected.

We will focus on three user interface behaviours for the most important user interface elements, designing our tests from the user point of view. We are are going to interact with the application, simulating a user, and performing assertions from a user’s point of view. The actions we are going to perform are:

  • - Tap the user interface.
  • - Scroll the application lists.
  • - Swipe the application left and right.


The assertions we can perform are:

  • - Look if an element is visible or not.
  • - Check if an element is showing the correct text.
  • - Check if the error messages are shown correctly.
  • - Check if the progress bar is indicating whether the content of the application is being loaded.
  • - Check if the lists show all the information obtained from the core of the system.


The tools we can use are Dagger 2 as a core component to replace production code with test doubles and Espresso to interact with the device and perform assertions. To use Dagger 2 easily for testing purposes we will use a JUnit rule named DaggerMock rule.

One of the key points to writing these tests is that when using a dependency injector we can configure the test doubles needed to create the initial scenario to exercise the subject under test. An example would be to configure the SuperHeroesRepository to return just one super hero when the getAll method is invoked.

@RunWith(AndroidJUnit4.class) @LargeTest public class MainActivityTest {

  private static final int ANY_NUMBER_OF_SUPER_HEROES = 10;

  @Rule public DaggerMockRule<MainComponent> daggerRule =
      new DaggerMockRule<>(MainComponent.class, new MainModule()).set(
          new DaggerMockRule.ComponentSetter<MainComponent>() {
            @Override public void setComponent(MainComponent component) {
              SuperHeroesApplication app =
                  (SuperHeroesApplication) InstrumentationRegistry.getInstrumentation()
                      .getTargetContext()
                      .getApplicationContext();
              app.setComponent(component);
            }
          });

  @Rule public IntentsTestRule<MainActivity> activityRule =
      new IntentsTestRule<>(MainActivity.class, true, false);

  @Mock SuperHeroesRepository repository;

  @Test public void showsEmptyCaseIfThereAreNoSuperHeroes() {
    givenThereAreNoSuperHeroes();

    startActivity();

    onView(withText("¯\\_(?)_/¯")).check(matches(isDisplayed()));
  }

  @Test public void showsSuperHeroesNameIfThereAreSuperHeroes() {
    List<SuperHero> superHeroes = givenThereAreSomeSuperHeroes(ANY_NUMBER_OF_SUPER_HEROES);

    startActivity();

    RecyclerViewInteraction.<SuperHero>onRecyclerView(withId(R.id.recycler_view))
        .withItems(superHeroes)
        .check(new RecyclerViewInteraction.ItemViewAssertion<SuperHero>() {
          @Override public void check(SuperHero superHero, View view, NoMatchingViewException e) {
            matches(hasDescendant(withText(superHero.getName()))).check(view, e);
          }
        });
  }


  private void givenThereAreNoSuperHeroes() {
    when(repository.getAll()).thenReturn(Collections.<SuperHero>emptyList());
  }
}


The first test checks whether there are no super heroes, an empty case is shown. The second test checks if when there are super heroes, the name of the super heroes is shown in the recycler view items. We are creating two different initial scenarios where the number of super heroes changes thanks to the usage of Dagger2, DaggerMock and Espresso. This allows us to test if the UI layer behaviour is implemented correctly based on the information mocked. At the end of the test execution we will assert if the user interface is showing the correct information. An important point to keep in mind is that these tests are possible thanks to the usage of testable code where the Dependency Inversion Principle usage is a core principle.

The usage of Espresso as a testing framework is also interesting. To perform actions or assertions in our tests we will use the Espresso API using the information shown in the screen and the identifiers used in our application layouts. The Espresso API is not that big, if you need more information about the Espresso matchers or assertions review this cheat sheet.

Another interesting point is the lack of features in the Espresso API needed to test the RecyclerView content. In this example we are using code published by Romain Piel in a gist because by default, the “onData” Espresso API does not support RecyclerView. Therefore we need to use this class or change our test code to move the application scroll to the view before to perform the assert. Even with this small issue, Espresso is the best testing framework we can use with headless continuous integration systems like Travis-CI or Circle-CI.



  @Test public void opensSuperHeroDetailActivityOnRecyclerViewItemTapped() {
    List<SuperHero> superHeroes = givenThereAreSomeSuperHeroes();
    int superHeroIndex = 0;
    startActivity();

    onView(withId(R.id.recycler_view)).
        perform(RecyclerViewActions.actionOnItemAtPosition(superHeroIndex, click()));

    SuperHero superHeroSelected = superHeroes.get(superHeroIndex);
    intended(hasComponent(SuperHeroDetailActivity.class.getCanonicalName()));
    intended(hasExtra("super_hero_name_key", superHeroSelected.getName()));
  }


Once we’ve tested that the information related to the super heroes is shown correctly, we need to check if the navigation between Activities is implemented correctly. In this test we have created an initial scenario where there are some super heroes and then we performed an action to tap a recycler view row. Once the super hero has been tapped we are using part of the Espresso Intents API to check if the intent generated by the application to start the SuperHeroDetailActivity activity contains the correct information. This approach to test if the navigation is properly implemented is quite interesting because we are basing our assertion on an implementation detail like the intent generated. Another valid approach is to check if the user interface shown after the tap is the one associated with the detail Activity.

Scope.

These tests have a large scope and could be consider end-to-end. We are testing simultaneously our presentation logic layer and all the code needed to interact with the Android SDK and to show information to the user. Another valid approach could be based on the intensive usage of the Dependency Inversion Principle. Following a pattern like Model View Presenter and writing testable code, we could replace our View implementation with a mock and verify that the information sent to the user interface is correct. But remember that to do so, we need to use a pattern like Model View Presenter or Model View View Model and move all our presentation logic to classes out of the framework like Presenters. Using this approach we could test our code using the Java Virtual Machine instead of the Android emulator.

Infrastructure.

The infrastructure needed is just a testable code and the usage of a dependency injector like Dagger or Dagger 2. The same approach could be reached without the usage of a dependency injector, but the code needed to replace production code with test doubles could be quite tedious to write. The use of DaggerMock rule is quite interesting to simplify the dependency injector initialization, but it is not mandatory.

Results.

When one is able to replace production code in a UI test, the way we test our user interface code is more interesting. Without the use of test doubles the approach to creating different test scenarios requires a provisioning API. The usage of a provisioning API means that we are going to move the test double to the server side and configure the provisioning server we are using before to exercise the subject under test.

Using the approach described in this post we can easily create reproducible test scenarios where our UI is tested in an isolated environment. MockWebServer could also be used to implement these tests, but the scope of the test would be so big that it would not be worth it. Similar to the provisioning API approach we could find problems with the cache mechanisms implemented in the application.

If you want to review the tests previously mentiones, check out the Kata we’ve designed for you >>> https://github.com/Karumi/KataSuperHeroesAndroid. We strongly recommend you to review the exercises we introduce in this Kata. If you are an iOS developer we’ve also prepared the same Kata but written in Swift and tested using KIF and Nimble as the core testing frameworks >>> https://github.com/Karumi/KataSuperHeroesIOS.

This is the last piece of the World-Class Testing Development Pipeline blog post series by Karumi. At this point we have successfully finished our Testing Development Pipeline. Now we know what to test and how to test our business logic creating isolated test scenarios, how to test our API integration using mocked http calls and how to test our user interface replacing production code with test doubles and performing assertions over the user interface instead of the software output.

P.S: It was my pleasure writing down all the ideas and best practices and sharing our experiences with you. Thank you so much for the ongoing support!

References.