Tutorial: Sustainable Android Tests with Page Objects



The content in this tutorial is based on ProTech's Android Testing course, which you can learn more about here.

When writing tests for your application—especially your application's user interface—you will quickly recognize that the same actions are often repeated multiple times. At the same time, the actions themselves may be leaking implementation details into your test logic that break the encapsulation that ensures your tests only exercise and verify the application's intended behavior.

As is always the case in software development, we can address this problem with another layer of abstraction.

What are Page Objects?

The PageObject concept was made popular by the Selenium browser automation framework. It is an abstraction layer that seeks to define an interface to trigger actions and obtain result states for a "page" (i.e. screen, or view). The pattern boils down to creating a series of helper methods that represent the functionality the test needs to invoke rather than asking the tests to execute the specific sequence of steps required for each individual action. Automated test code is written to interact directly with the page object rather than the underlying view or component under test.

The page object abstraction has two primary advantages:

  1. Code Reuse: Actions that get triggered often across the scope of multiple test cases can lead to repeating the same code snippet to execute the sequence of steps. These actions are not really related to the test itself, but must still be executed in order to set up the state that needs to be verified.
  2. Clear Separation: Actions with a complex set of steps may be leaking the details of the application's implementation into the test. The order of operations in executing the action is not of interest to the test code, and may cause the test to break in the future if the sequence of steps to perform the same action changes.
As a side benefit, this tends to make your test code more readable and easier to parse for a developer who is unfamiliar with the codebase.

Example

Let's see what this would look like in an Android user interface test case:

public class MainActivityTest {
  @Rule
  public ActivityTestRule activityRule =
        new ActivityTestRule<>(MainActivity.class);

  @Test
  public void settingsActionShowsSettingsView() {
    // open overflow menu
    Context context = InstrumentationRegistry.getTargetContext();
    Espresso.openActionBarOverflowOrOptionsMenu(context);
    // click settings item
    onView(allOf(withText(R.string.action_settings), withId(R.id.title)))
          .perform(click());

    // …verify new view is displayed…
  }

  @Test
  public void settingsActionIsInOverflow() {
    // open overflow menu
    Context context = InstrumentationRegistry.getTargetContext();
    Espresso.openActionBarOverflowOrOptionsMenu(context);

    // …verify settings item is displayed…
  }

  @Test
  public void postActionShowsPostView() {
    // open overflow menu
    Context context = InstrumentationRegistry.getTargetContext();
    Espresso.openActionBarOverflowOrOptionsMenu(context);
    // click post item
    onView(allOf(withText(R.string.action_post), withId(R.id.title)))
          .perform(click());

    // …verify new view is displayed…
  }
}

Here, we are repeating the same steps at different stages in multiple tests cases. Abstracting this into a page object would look like this:

public class MainPageObject {

  private static void clickOverflowItem(int resId) {
    onView(allOf(withText(resId), withId(R.id.title)))
          .perform(click());
  }
  
  // overflow menu
  public static void openOverflowMenu() {
    Context context = InstrumentationRegistry.getTargetContext();
    Espresso.openActionBarOverflowOrOptionsMenu(context);
  }

  // settings overflow item
  public static void navigateToSettings() {
    clickOverflowItem(R.string.action_settings);
  }

  // post overflow item
  public static void navigateToPost() {
    clickOverflowItem(R.string.action_post);
  }
}

We can then replace the duplicated code inside our test:

public class MainActivityTest {
  @Rule
  public ActivityTestRule activityRule =
        new ActivityTestRule<>(MainActivity.class);

  @Test
  public void settingsActionShowsSettingsView() {
    // open overflow menu
    MainPageObject.navigateToSettings();

    // …verify new view is displayed…
  }

  @Test
  public void settingsActionIsInOverflow() {
    // open overflow menu
    MainPageObject.openOverflowMenu();

    // …verify settings item is displayed…
  }

  @Test
  public void postActionShowsPostView() {
    // open overflow menu
    MainPageObject.navigateToPost();

    // …verify new view is displayed…
  }
}

Now, a code change need only be made in one place if the sequence for any particular action has to change in the future. Notice that the verification logic is not abstracted. This code remains inside the test case as it reflects the purpose of the test itself. Only the triggering actions required to set up the initial state have been pulled into the page object classes.

It's the little things that make all the difference in the long run. Page objects may just be a fancy name for a concept that is not necessarily new to the average software developer (we all strive for our code to by DRY, right?). However, it is a good reminder to all of us that the principles we hold dear in writing good, maintainable software should be applied just as readily to the code we write for automated tests. Test automation is as much a part of the software development effort as adding any new feature!

Become an expert in automated testing on the Android platform in our Android Testing course!

Published October 16, 2015