In this chapter we will:
explain the Page Object Design Pattern (alternately known as the Screen Object Design Pattern)
examine AndroidJUnitRunner in detail including JUnit rules and the Android Test Orchestrator
ways to apply the screen object pattern to our test cases
how to create custom annotations for our test cases
how to run multiple test cases using a test suite
Every functional unit of a displayed page should be wrapped by a page object.
The page object allows you to interact with the UI and promotes code reusability.
In the above image we have three screens or fragments.
To apply the page object pattern here we need to create a page object class for each of the three fragments.
Each class should implement methods corresponding to possible user interactions in each fragment (for example, text entry, tab switching, or button clicks).
Each method may implement assertions on user input or expected UI layout and behavior as required by the use case.
When a UI interaction requires navigating to another fragment, the appropriate page object must be returned by that method.
AndroidJUnitRunner is a JUnit test runner class enabling the execution of JUnit test cases on Android devices, including those supported by Espresso.
The test runner handles loading of test packages and applications under test to a device, test case execution, and test results reporting.
AndroidX Test includes a set of JUnit rules to be used with AndroidJUnitRunner. JUnit rules provide additional flexibility and reduce the complexity of test code.
Android Test Orchestrator enables each of your app's tests to execute within its own invocation of Instrumentation. This has the following benefits:
Minimal shared state: Each test runs in its own Instrumentation instance. To remove all shared states from your device's CPU and memory after each test, use the clearPackageData
flag from AndroidJUnitRunner.
Crashes are isolated: Even if one test crashes, it takes down only its own instance of Instrumentation so that the other tests in your suite can still run.
Android Test Orchestrator collects JUnit tests at the beginning of a test suite run and executes each test separately in its own instance of Instrumentation.
In this demo we will apply the page object design pattern to our Espresso test cases.
Begin by running the "mailordercoffee-workshop" sample application and explore the interface. This app takes orders for a coffee shop.
Orders submitted with the app will prompt the user to complete order notifications via email.
The initial screen is simply an onboarding splash screen.
We need to click the "X" button to close it and continue on with our order so let’s do that now.
On the next screen there are two buttons, "CUSTOM ORDER" and "MENU".
Note that "Custom Order" is the default view and we are already looking at it, which is why the "CUSTOM ORDER" button is not clickable.
Clicking on the "MENU" button will bring up a RecyclerView list of all possible order items.
Back on the "Custom Order" screen you can create a custom order.
For example, add four espresso shots and let’s check "Chocolate" for good measure.
Now click "REVIEW ORDER" to review your order.
Now we need to fill in the various fields here and click on "SUBMIT ORDER".
The next screen is an email composition screen with various fields populated using the text we entered earlier.
Notice that the body of the email is automatically populated with the items in our order.
Experiment with adding additional order items from the app to see how they change the contents of the message body.
Our application test cases should cover these scenarios.
For tests involving the email component we will use intents to avoid calling a third-party email client.
Before we start, check the Espresso dependencies in our "build.gradle" file.
Verify that the dependency for Android Orchestrator is present:
androidTestUtil ‘androidx.test:orchestrator:1.2.0’
Ensure that under android’s "defaultConfig" section the instrumentation runner argument shown below is added to clear application package data in between test case runs.
Also, specify the test option to perform execution using Android Test Orchestrator which will split our test cases and run each in isolation.
testInstrumentationRunnerArguments clearPackageData: ‘true’
dataBinding {
enabled = true
}
testOptions {
execution ‘ANDROIDX_TEST_ORCHESTRATOR’
}
Now let’s look at the currently implemented test cases in our project.
We already have three test classes in our project under the "androidTest" package, "CreateCustomOrderWithIngredientsTest", "OrderItemFromMenuTheTest", and "SendOrderToEmailTest".
These class implementations are highly similar to those from previous demos and include similar AndroidJUnit runner declarations, intent definitions, RecyclerView manipulations and various assertions.
Notice that the demo app’s initial onboarding splash screen, once closed, will not open again.
This represents some inconsistency in test case procedures because the close button ("X") will not be displayed after the first invocation of the app.
To fix this, our intent requires overriding the method beforeActivityLaunched()
to manipulate certain shared preferences that will force the onboarding screen to display on every application launch.
This is what it will look like in class "OrderItemFromMenuTheTest" when setting a value for "is_first_launch":
@Rule
public IntentsTestRule<MainActivity> activityTestRule =
new IntentsTestRule<MainActivity>(MainActivity.class) {
@Override
protected void beforeActivityLaunched() {
super.beforeActivityLaunched();
Context context = InstrumentationRegistry.getInstrumentation()
.getTargetContext();
SharedPreferences.Editor editor = context
.getSharedPreferences(context.getPackageName(),
Activity.MODE_PRIVATE).edit();
editor.putBoolean("is_first_launch", true);
editor.commit();
}
};
Let’s go ahead and run the test case shouldBeAbleToSelectAnItemInTheMenu()
to see what happens.
The automated steps will close the onboarding screen, click on the "MENU" button, add an item and then perform its view assertions.
With our current configuration using Android Orchestrator the test appears to pass.
Now change the value of "is_first_launch" to "false" and re-run the test to see what happens.
editor.putBoolean("is_first_launch", false);
The test now fails because the application goes straight to the "Custom Order" activity on startup, bypassing the onboarding splash screen entirely.
Let’s change the value back to "true". This capability offers quite a lot of flexibility in choosing how to test different screens.
For example, bypassing the onboarding screen and proceeding directly to the "Custom Order" menu might be desirable for a different test case.
The second test involves creating a custom order with ingredients.
If we inspect the source code it uses the same beforeActivityLaunched()
method as the previous class.
The automated test procedure closes the onboarding screen, adds two shots of espresso plus chocolate, clicks the "REVIEW ORDER" button, then performs an assertion of the ingredients list text appearing on the next screen against our verification text.
Let's run the test and see what happens. The test should pass successfully.
The third test involves sending orders to email.
The overall test structure is highly similar to the previous two tests.
The automated test procedure closes the onboarding screen, adds four shots of espresso, subtracts one, adds chocolate, and finally clicks on "REVIEW ORDER".
On the next screen the various text fields are populated and finally the "SUBMIT ORDER" button is clicked.
Remember that we are performing our assertion against an Intent action and do not actually need to call the email client.
"ACTION_SENDTO" is exactly what we need here. Values of applicable email text fields like "EXTRA_SUBJECT" are asserted here as well.
Let's run our test and check the test result. It should pass without incident.
package tests;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.test.espresso.intent.rule.IntentsTestRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import nl.testchamber.mailordercoffeeshop.MainActivity;
import nl.testchamber.mailordercoffeeshop.R;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
@RunWith(AndroidJUnit4.class)
public class CreateCustomOderWithIngredientsTest {
@Rule
public IntentsTestRule<MainActivity> activityTestRule =
new IntentsTestRule<MainActivity>(MainActivity.class) {
@Override
protected void beforeActivityLaunched() {
super.beforeActivityLaunched();
Context context = InstrumentationRegistry.getInstrumentation()
.getTargetContext();
SharedPreferences.Editor editor = context
.getSharedPreferences(context.getPackageName(),
Activity.MODE_PRIVATE).edit();
editor.putBoolean("is_first_launch", true);
editor.commit();
}
};
@Test
public void orderOverViewShouldDisplayedIngredients() {
onView(withId(R.id.close_button))
.perform(click());
onView(withText("+"))
.perform(click(), click());
onView(withId(R.id.chocolate))
.perform(click());
onView(withText("Review order"))
.perform(click());
onView(withId(R.id.beverage_detail_ingredients))
.check(matches(withText("Ingredients:\n2 shots of espresso\nChocolate")));
}
}
package tests;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.test.espresso.intent.rule.IntentsTestRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import nl.testchamber.mailordercoffeeshop.MainActivity;
import nl.testchamber.mailordercoffeeshop.R;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem;
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
@RunWith(AndroidJUnit4.class)
public class OrderItemFromMenuTheTest {
@Rule
public IntentsTestRule<MainActivity> activityTestRule =
new IntentsTestRule<MainActivity>(MainActivity.class) {
@Override
protected void beforeActivityLaunched() {
super.beforeActivityLaunched();
Context context = InstrumentationRegistry.getInstrumentation()
.getTargetContext();
SharedPreferences.Editor editor = context
.getSharedPreferences(context.getPackageName(),
Activity.MODE_PRIVATE).edit();
editor.putBoolean("is_first_launch", true);
editor.commit();
}
};
@Test
public void shouldBeAbleToSelectAnItemInTheMenu() {
onView(withId(R.id.close_button))
.perform(click());
onView(withId(R.id.use_menu))
.perform(click());
onView(withId(R.id.beverage_recycler_view))
.perform(actionOnItem(hasDescendant(withText("CAPPUCCINO"))
, click()));
onView(withId(R.id.beverage_detail_title))
.check(matches(withText("Cappuccino")));
}
}
package tests;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.test.espresso.intent.rule.IntentsTestRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import nl.testchamber.mailordercoffeeshop.MainActivity;
import nl.testchamber.mailordercoffeeshop.R;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.scrollTo;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.intent.Intents.intended;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.equalTo;
@RunWith(AndroidJUnit4.class)
public class SendOrderToEmailTest {
@Rule
public IntentsTestRule<MainActivity> activityTestRule =
new IntentsTestRule<MainActivity>(MainActivity.class) {
@Override
protected void beforeActivityLaunched() {
super.beforeActivityLaunched();
Context context = InstrumentationRegistry.getInstrumentation()
.getTargetContext();
SharedPreferences.Editor editor = context
.getSharedPreferences(context.getPackageName(),
Activity.MODE_PRIVATE).edit();
editor.putBoolean("is_first_launch", true);
editor.commit();
}
};
@Test
public void shouldSendAnIntentContainingTheRightOrderDetails() {
onView(withId(R.id.close_button))
.perform(click());
for (int i = 0; i < 4; i++) {
onView(withText("+"))
.perform(click());
}
onView(withText("-"))
.perform(click());
onView(withId(R.id.chocolate))
.perform(click());
onView(withText("Review order"))
.perform(click());
onView(withId(R.id.name_text_box))
.perform(scrollTo(), typeText("Moataz"));
onView(withId(R.id.custom_order_name_box))
.perform(scrollTo(), typeText("TAU Order Name"));
onView(withId(R.id.mail_order_button))
.perform(scrollTo(), click());
intended(allOf(
hasAction(equalTo(Intent.ACTION_SENDTO)),
hasExtra(Intent.EXTRA_SUBJECT, "Order: Moataz - TAU Order Name")));
}
}
Quiz
The quiz for this chapter can be found in section 7.3