In my previous blog, I wrote about the basics of Espresso and how we can set it up and use it.
In this blog, we will look at how we can test other components using Espresso. We will be covering some basic tests we might need to write while writing test cases. We will also be covering how to test views that are not part of the default window hierarchy along with testing AdapterViews & RecyclerViews. Lastly, we will be covering how we can write custom Matchers and custom Failure Handlers.
Basic Tests
Let’s first look at some common tests we might need to write:
//Perform click event on view with id R.id.btn_login onView(withId(R.id.btn_login)) .perform(click()); //Type text in view with id R.id.et_name onView(withId(R.id.et_name)) .perform(typeText("Espresso"), closeSoftKeyboard()); //Check that text in view with id R.id.et_name contains text "Espresso" onView(withId(R.id.et_name)) .check(matches(withText(containsString("Espresso")))); //Perform click event on a view that is a sibling of another view onView(allOf(withText("Espresso"), hasSibling(withText("sibling item: 10")))) .perform(click()); //Asserting that a view is not displayed onView(withId(R.id.btn_login)) .check(matches(not(isDisplayed()))); //Asserting that a view is not present onView(withId(R.id.btn_login)) .check(doesNotExist());
Following the pattern above, we can write test cases according to our requirements.
Test Views Outside Default Window Hierarchy
There might be a few cases in which we need to test views that are not part of the default window hierarchy. For example, testing a view that might have been rendered using WindowManager:
onView(withText("Espresso")) .inRoot(withDecorView( not(is(getActivity().getWindow().getDecorView())))) .perform(click());
Testing an AdapterView
Now let’s move on to AdapterView testing. To test AdapterView, Espresso provides a separate onData() entry point which first brings the adapter item to be tested in focus before performing any operation on itself or its children. So in case we want to test an AdapterView, we need to use the onData() method instead of the onView() method.
onData(ObjectMatcher) .DataOptions .perform(ViewAction) .check(ViewAssertion);
A complete list of the available ObjectMatcher, DataOptions, ViewAction, and ViewAssertion can be found in this Espresso Cheat Sheet.
Let’s have a look at a simple example that finds a list item of type String matching the word “Espresso”, and performing a click() event on it:
onData(allOf(is(instanceOf(String.class)), ("Espresso"))) .perform(click());
The below example will find the list item having content “item content: 10” and perform a click event on its child view having id R.id.item_id:
onData(withItemContent("item content: 10")) .onChildView(withId(R.id.item_id)) .perform(click());
Testing a RecyclerView
As RecyclerView objects behave differently than AdapterView objects, we cannot use onData() to test RecyclerView objects.
We need to add the espresso-contrib package dependency if we want to test RecyclerView. This package includes a collection of RecyclerViewActions that we can use to scroll to positions or to perform actions on the items.
To interact with RecyclerViews using Espresso, you can use the espresso-contrib package (add ‘com.android.support.test.espresso:espresso-contrib:2.2.2’ dependency in app/build.gradle), which has a collection of RecyclerViewActions that can be used to scroll to positions or to perform actions on items. This collection includes the following RecylerViewActions:
- scrollTo() – This RecyclerViewActions scrolls to the matched View.
- scrollToHolder() – This RecyclerViewActions scrolls to the matched View Holder.
- scrollToPosition() – This RecyclerViewActions scrolls to a specific position.
- actionOnHolderItem() – This RecyclerViewActions performs a View Action on a matched View Holder.
- actionOnItem() – This RecyclerViewActions performs a View Action on a matched View.
- actionOnItemAtPosition() – This RecyclerViewActions performs a ViewAction on a view at a specific position.
Let’s look at an example of how we can test a RecyclerView:
onView(ViewMatchers.withId(R.id.rv_espresso)) .perform( RecyclerViewActions.actionOnItemAtPosition(10, click()));
The above example will first find the RecyclerView with id R.id.rv_espresso, then scroll to its 5th position and perform a click event on that item.
Similarly, we can use other RecyclerViewActions as well.
Now let’s see how we can test a custom RecyclerView, for example, an ExpandableRecyclerView. Let’s consider the case when we need to test some child view of the expandable group view.
Firstly, we will write a custom Action that we can perform:
public class ChildViewAction { public static ViewAction clickChildViewWithId( final int id) { return new ViewAction() { @Override public Matcher<View> getConstraints() { return null; } @Override public String getDescription() { return "Click on a child view with specified id."; } @Override public void perform(UiController uiController, View view) { View v = view.findViewById(id); v.performClick(); } }; } }
Now, we will perform this action on the child view:
//Expands group at position 0 by performing click action onView(withId(R.id.rv_espresso)) .perform( RecyclerViewActions.actionOnItemAtPosition(0, click())); //Performs click action on child view at position 0 of group 0 onView(withId(R.id.rv_espresso)) .perform( RecyclerViewActions.actionOnItemAtPosition(1, ChildViewAction.clickChildViewWithId( R.id.view_child_item)));
Now let’s have a look at how we can write and use a Custom Matcher and a Custom Failure Handler.
Custom Matcher & FailureHandler
We might need to implement a Custom Matcher or a Custom Failure Handler while writing our test cases as the default ones might not suffice in some cases.
1. Custom Matcher
There might be cases when the default available Matchers are not sufficient. In this case, we might need to write our own matcher. Let’s consider a case when we need to check that the text inside an EditText matches a regular expression; since there is no default matcher that does this for us, we will need to give our own implementation for it.
Let’s see how we can write a custom matcher for validating a pattern:
public class PatternMatcher { static Matcher<View> withPattern( final String pattern) { checkNotNull(pattern); return new BoundedMatcher<View, EditText>( EditText.class) { @Override public void describeTo( Description description) { description .appendText( "The EditText does not conform to the pattern: ") .appendText(pattern); } @Override protected boolean matchesSafely( EditText item) { return item .getText() .toString() .matches(pattern); } }; } }
Now let’s see how we can use this matcher:
onView(withId(R.id.et_email)) .check(matches( PatternMatcher.withPattern( Patterns.EMAIL_ADDRESS.toString())));
2. Custom Failure Handler
There might be cases in which we might want to handle some Exception that Espresso throws and give our own implementation for it, like logging some extra data that might be more meaningful.
private static class CustomFailureHandler implements FailureHandler { private final FailureHandler delegate; public CustomFailureHandler(Context targetContext) { delegate = new DefaultFailureHandler(targetContext); } @Override public void handle(Throwable error, Matcher<View> viewMatcher) { try { delegate.handle(error, viewMatcher); } catch (NoMatchingViewException e) { throw new MySpecialException(e); } } }
Now that we have written a custom failure handler, let’s see how we can set it up.
@Override public void setUp() throws Exception { super.setUp(); getActivity(); setFailureHandler( new CustomFailureHandler( getInstrumentation().getTargetContext())); }
This was all about how to write basic test cases for different components. I hope this blog motivates you to write test cases for your applications. At first, it might be difficult, but once you get a hang of it, it will be of great use.
You can refer to this link for more on Espresso for Android UI Testing.
Hope you enjoyed reading.