📜 ⬆️ ⬇️

Espresso: “Cute little animals or dangerous predators?”

Good day, readers Habra! Today we will test Recyclerview on Android together: in my opinion, this topic is quite interesting.



What is Recyclerview? This is the component with which lists are created. Each list can be scrolled, add items to it, delete them or change them. An element is any functional unit. For example, we make a list of users with a field for entering a comment and a button. As soon as the comment is entered and the button is pressed, it is sent to the server. The system can now upgrade or delete an item.
Elements can contain many controls (such as buttons), so all the capabilities of the elements must be covered with tests. Now I will share with you useful utilities for Recyclerview.

Example


As an example, take a simple application that displays a list of animals. List data is an array of similar objects. So what do we see?
')
[ ... { "type": "PREDATOR", "name": "", "image_url": "https://www.myplanet-ua.com/wp-content/uploads/2017/05/%D0%B2%D0%B8%D0%B4%D1%8B-%D1%82%D0%B8%D0%B3%D1%80%D0%BE%D0%B2.jpg" }, { "type": "HERBIVORE", "name": "", "image_url": "https://cs2.livemaster.ru/storage/b2/40/b9d72365ffc02131ea60420cdc0s--kukly-i-igrushki-suslik-bejli-igrushka-iz-shersti.jpg" }, .... ] 

Element consists of:

  1. Animal species - PREDATOR (predator) and HERBIVORE (herbivore). Depending on it, one or another icon is assigned to the animal.
  2. Animal names (tiger, gopher, etc.).
  3. Links to the image of the animal.

Users see the following:


When you click on the "Delete" button, the picture disappears from the list. Clicking on the animal itself opens a more detailed description of it.

We proceed to testing:

  1. Let's see how the item is deleted.
  2. Check if the icon matches the animal’s species (green smiley - herbivore, red smiley - predator).
  3. Check if the name matches the given one.

For convenience, I mark the test areas in the image below:



So, we start writing auxiliary classes.

DrawableMatcher


The first important auxiliary class will be checking for compliance with an image with a local resource. The correspondence of the type field (response from the server) and the name of the local resource is as follows:

PREDATOR - ic_sentiment_very_dissatisfied_red_24dp
HERBIVORE - ic_sentiment_very_satisfied_green_24dp

Example: in our task we are sure that the capybara is a herbivore, and therefore in the imageView element with the identifier ivAnimalType, the value ic_sentiment_very_satisfied_green_24dp should be set.

In fact, it is necessary to verify the compliance of the established resource depending on the type of animal. Since for this I did not find the standard tools, I had to write my Matcher, which checks the background properties of the imageView element for compliance with the transmitted identifier.

Slight reservation


Matcher is most often used to check the specific properties of a custom view against the parameters passed.

Example: we wrote our own custom view representing the door. It can be in two states: open and closed. To check the door manipulations, we can write our own Matcher. Having agreed that after the handle is turned, the door goes from closed to open, in our test we pull the handle and, using our own matcher, check that it is really open.

To write this kind of matcher, I use the standard class of the org.hamcrest package - TypeSafeMatcher. More information about him can be found here.

This class is a generic class with a custom view type (imageView, view, or others). It provides 2 basic methods:

  1. matchesSafely is the root method in which the match is checked. This parameter is passed an object of type generic class.
  2. describeTo is a function for the ability of an object to describe itself. Called when the match is not found and, preferably, contains the name of what we are trying to find, and what we have actually met.

In this case, it seems to me, it is advisable to transfer the resource identifier, which we will further check, to the constructor of our class.

 public DrawableMatcher(final int resourceId) { super(View.class); mResDrawableId = resourceId; } 

The description method is very convenient when debugging a test. For example, if there is a discrepancy between the images, it displays a message stating that the installed resource does not match the scanned resource, and gives some data from both resources.

 @Override public void describeTo(Description description) { description.appendText("with drawable from resource id: "); description.appendValue(mResDrawableId); if (resourceName != null) { description.appendText("["); description.appendText(resourceName); description.appendText("]"); } } 

In the root method of compliance checking, the standard asAs method of the Bitmap class is checked. That is, we create a bitmap using the passed identifier and compare it with the one set in the background field.

 protected boolean matchesSafely(final View target) { if (!(target instanceof ImageView)) { return false; } final ImageView imageView = (ImageView) target; if (mResDrawableId < 0) { return imageView.getBackground() == null; } final Resources resources = target.getContext().getResources(); final Drawable expectedDrawable = resources.getDrawable(mResDrawableId); resourceName = resources.getResourceEntryName(mResDrawableId); if (expectedDrawable == null) { return false; } final Bitmap bitmap = getBitmap(imageView.getBackground()) final Bitmap otherBitmap = getBitmap(expectedDrawable); return bitmap.sameAs(otherBitmap); } 

That's all, as for checking the compliance of the image with the specified resource.

Internal element events


Since, according to the task, we will need to click on the “delete” button, we need to write an additional implementation of ViewAction. It was convenient for me to make a certain class of utilities called CustomRecyclerViewActions. It contains many static methods that return a ViewAction implementation. In our example, we will use clickChildViewWithId.
First, let's look at the ViewAction interface in more detail. It consists of the following methods:

  1. Matcher getConstraints () - some preconditions for execution. For example, it may be necessary for the element on which the action will be performed to be visible.
  2. String getDescription () - description of the view action. Required to create a readable display of messages in the log.
  3. void perform (UiController uiController, View view) is the action itself that we will perform.

So let's go back to writing the clickChildViewWithId method. In the parameters, I pass the ID of the custom view on which I will execute the click event. Full implementation of this method can be viewed.
here
 public static ViewAction clickChildViewWithId(final int id) { return new ViewAction() { @Override public Matcher<View> getConstraints() { return null; } @Override public String getDescription() { return "     id"; } @Override public void perform(final UiController uiController, final View view) { final View v = view.findViewById(id); v.performClick(); } }; } 


Checking the number of items inside the adapter


We also need to check the number of items inside the Recyclerview. To do this, we need our own implementing ViewAssertion interface. Let's call it RecyclerViewItemCountAssertion.

The ViewAssertion interface is one method:
void check(View view, NoMatchingViewException noViewFoundException);

Consider the check method in more detail. This method of verifying user assertions takes 2 parameters:

  1. View - an element of the custom view that will be asserted (for example, assertThat(0,is(view.getId()) ); for identifier verification).
  2. NoMatchingViewException - an exception that occurs when a given resolver is not a member of the view hierarchy; in other words, it is not a View. Such an exception contains information about the presentation and compliance, which is very convenient when debugging.

The implementation of this class is simple. You can see the full class code.
here
 public class RecyclerViewItemCountAssertion implements ViewAssertion { private final int mExpectedCount; public RecyclerViewItemCountAssertion(final int expectedCount) { mExpectedCount = expectedCount; } @Override public void check(final View view, final NoMatchingViewException noViewFoundException) { if (noViewFoundException != null) { throw noViewFoundException; } final RecyclerView recyclerView = (RecyclerView) view; final RecyclerView.Adapter adapter = recyclerView.getAdapter(); assertThat(adapter.getItemCount(), is(expectedCount)); } } 


Check internal element views


We now turn to the study of the class for checking the elements inside the list. This class I called RecyclerViewItemSpecificityView. The class constructor takes 2 parameters: the element identifier and the Matcher. With the identifier, everything is more or less clear: we will subsequently check this element. And the second parameter answers the question what exactly we are going to check. Let's move on to the example of animals.

We need to check that the 6th item in the list is capybara, herbivore. In order to verify this, you need to check the tvName field for compliance with the text “capybara”. How can I get to the 6th element? The espresso.contrib package has a class called RecyclerViewActions. It helps with testing RecyclerView, contains many different useful methods that are perfectly combined with the standard methods of the espresso library. This allows you to achieve a higher percentage of coverage. Unfortunately, RecyclerViewUtils does not support the full range of functionality. For example, you cannot directly check items inside a list card in an explicit form.

Consider the RecyclerViewItemSpecificityView class. It is an implementation of the ViewAssertion interface. Accordingly, it operates according to the same scheme as RecyclerViewItemCountAssertion, but has a different purpose.

The constructor of the RecyclerViewItemSpecificityView class, as mentioned earlier, takes 2 parameters and represents the following construction:

 public RecyclerViewItemSpecificityView(final int specificallyId, final Matcher<View> matcher) { mSpecificallyId = specificallyId; mMatcher = matcher; } 

The check method happens like this:

  1. Search for the corresponding item by mSpecificallyId. Here we will pass the identifiers of the element Recyclerview.
  2. MMatcher compliance check. This check is the main task of the class.
  3. Formation of readable conclusions in the absence of compliance.

 @Override public void check(final View view, final NoMatchingViewException noViewFoundException) { final StringDescription description = new StringDescription(); description.appendText("'"); mMatcher.describeTo(description); final ViewGroup itemRoot = (ViewGroup) view; final View typeIUsage = itemRoot.findViewById(mSpecificallyId); if (noViewFoundException != null) { description.appendText( String.format( "' check could not be performed because view with id '%s' was not found.\n", mSpecificallyId)); Log.e("RecyclerViewItemSpecificityView", description.toString()); throw noViewFoundException; } else { description.appendText("' doesn't match the selected view."); assertThat(description.toString(), typeIUsage, mMatcher); } } 

“Well, is it still a capybara?”


All elements are ready, now let's try to check our capybara. To begin with, we will write a test that checks whether an element is a herbivorous capybara.

 onView(withId(R.id.rvAnimals)) .perform(scrollToPosition(capybaraPosition)) .check(new RecyclerViewItemSpecificityView(R.id.tvName, withText(""))) .check(new RecyclerViewItemSpecificityView(R.id.ivAnimalType, new DrawableMatcher(R.drawable.ic_sentiment_very_satisfied_black_24dp))); 

ScrollToPosition is a method of the RecyclerViewActions class. It is needed to scroll the list to the selected position. If the list item is not visible, then test it will not work. Next, we check that the item to which we scrolled the list in the tvName field contains the string "Capybara". Also, the element under test is “herbivorous,” so we have to check that the icon (ivAnimalType) is ic_sentiment_very_satisfied_black_24dp.

Now we write a test to delete the item. I think you have already guessed how we can use the CustomRecyclerViewActions class clickChildViewWithId static method and verify that the number of items has decreased using RecyclerViewItemCountAssertion.

 onView(withId(R.id.rvAnimals)).check(new RecyclerViewItemCountAssertion(8)); onView(withId(R.id.rvAnimals)).perform( RecyclerViewActions.actionOnItemAtPosition(0, MyRecyclerViewActions.clickChildViewWithId(R.id.btnRemove))); onView(withId(R.id.rvAnimals)).check(new RecyclerViewItemCountAssertion(7)); 

I specifically used the actionOnItemAtPosition method of the RecyclerViewActions class. He scrolls the list to the current position and thus gives us the opportunity to manipulate the list item.

Conclusion


Testing is a very important process.
“The task cannot be completed until it is covered by at least 70% of the test”
- So the boss told me when I just got acquainted with the wonderful world of programming. In my opinion, an important test criterion is the coverage area. In the first place - checking the main functionality of the tested part of the software, then - trying to introduce the application in some emergency situations. This improves the quality of the software product, its sustainability and, most importantly, understanding. Yes, and what a sin to confess: if the program is covered with tests, sleep much more easily.

Today we talked about how to get closer to creating a “tester's suitcase,” and I hope you found something useful for yourself to complement your set. You can read the full example here.

Source: https://habr.com/ru/post/351888/


All Articles