Espresso: “Cute little animals or dangerous predators?”

    Good day, readers of Habr! Today we are going to test Recyclerview on Android together with you: in my opinion, this topic is quite interesting.



    What is a Recyclerview? This is the component with which lists are created. You can scroll through each list, 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 remove 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 objects of the same type. 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"
    },
    ....
    ]
    

    The element consists of:

    1. Species of the animal - PREDATOR (predator) and HERBIVORE (herbivore). Depending on it, the animal is assigned one or another icon.
    2. Names of the animal (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. When you click on the animal itself, a more detailed description opens.

    We pass to testing:

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

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



    So, we begin writing auxiliary classes.

    DrawableMatcher


    The first important helper class will be to check if the image matches the 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 herbivorous animal, and therefore should be set to identie_identifier identifier_identifier_identifier_identifier_identifier_id

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

    Small reservation


    Matcher is most often used to check the specific properties of a user view for compliance with the parameters passed.

    Example: we wrote our own custom view depicting a door. It can be in two states: open and closed. To check the manipulation of the door, 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 package org.hamcrest - TypeSafeMatcher. More details can be found here.

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

    1. matchesSafely is the root method in which the compliance check passes. An object of type generic class is passed to this parameter.
    2. describeTo is a function for the ability of an object to describe itself. It is called when there is an unknown match and, preferably, contains the name of what we are trying to find, and what we met in fact.

    In this case, it seems to me that it is advisable to pass the identifier of the resource, 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 the images do not match, he displays a message stating that the installed resource does not match the one being checked, and gives some data of 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 conformance check method, the standard sameAs method of the Bitmap class is checked. That is, we create a bitmap by the transmitted 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 with regard to checking the image for a given resource.

    Events of internal elements


    Since, according to the assignment, we need to click on the “delete” button, we need to write an additional implementation of ViewAction. It was convenient for me to make a class of utilities called CustomRecyclerViewActions. It contains many static methods that return ViewAction implementations. In our example, we will use clickChildViewWithId.
    To get started, 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 required that the element over which the action will be performed is visible.
    2. String getDescription () - a description of the action of the view. Required to create a readable display of messages in the log.
    3. void perform (UiController uiController, View view) - directly the action itself that we will perform.

    So let's get back to writing the clickChildViewWithId method. In the parameters I pass the identifier of the user view, on which I will execute the click event. The full implementation of this method can be seen
    here.
    public static ViewAction clickChildViewWithId(final int id) {
    	return new ViewAction() {
    		@Override
    		public Matcher 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 elements inside the adapter


    We also need to check the number of elements inside the Recyclerview. To do this, we will 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 checking user claims takes 2 parameters:

    1. View - an element of the user view over which the statement will be made (for example, assertThat(0,is(view.getId())); to verify the identifier).
    2. NoMatchingViewException - an exception that occurs when a given mapper is not an element of the hierarchy of views, in other words, it is not a View. This exception contains information about the presentation and compliance, which is very convenient for debugging.

    The implementation of this class is simple. The full class code you can see
    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));
    	}
    }


    Checking the internal representations of an element


    We proceed to study the class of checking elements inside the list. This class I called RecyclerViewItemSpecificityView. The class constructor takes 2 parameters: element identifier and 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 are we going to check. Let's move on to the example of animals.

    We need to verify that the 6th element of the list is “capybara”, a herbivore. In order to verify this, you need to check the tvName field for the correspondence of the text “capybara”. How can I reach exactly the 6th element? The espresso.contrib package has a RecyclerViewActions class. It helps with testing RecyclerView, it contains many different useful methods that combine perfectly with the standard methods of the espresso library. This allows you to achieve a greater percentage of coverage. Unfortunately, RecyclerViewUtils does not support the full range of functionality. For example, you cannot directly check the elements inside the list card explicitly.

    Consider the RecyclerViewItemSpecificityView class. It is an implementation of the ViewAssertion interface. Accordingly, it acts in the same way as the 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 matcher) {
    	mSpecificallyId = specificallyId;
    	mMatcher = matcher;
    }

    The check method in this case happens like this:

    1. Searches for the corresponding element by identifier mSpecificallyId. Here we will pass the identifiers of the Recyclerview element.
    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, all the same - is it a capybara?”


    All the elements are ready, let's now try to check our capybara. To begin with, we will write a test that checks whether the 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 it will not work. Next, we check that the element to which we scrolled the list in the tvName field contains the string "Capybara". Also the item under test is “herbivore”, so we need to check that the icon (ivAnimalType) matches ic_sentiment_very_satisfied_black_24dp.

    Now let's write a test to delete an item. I think you have already guessed how we can use the static clickChildViewWithId method of the CustomRecyclerViewActions class and verify that the number of elements has been reduced 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 thereby 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% test”
    - so the chief told me when I was just getting acquainted with the wonderful world of programming. In my opinion, an important testing criterion is the coverage area. First of all - checking the basic functionality of the tested part of the software product, then - trying to introduce the application in some emergency situations. This improves the quality of the software product, its stability and, most importantly, understanding. Yes, and what a sin to hide: if the program is covered with tests, it sleeps much more calmly.

    Today we talked about how to approach the creation of a “tester suitcase”, and I hope you have found something useful for yourself to complement your kit. You can find a complete example here.

    Also popular now: