How to access RecyclerView ViewHolder with Espresso?

10,861

Solution 1

Espresso package espresso-contrib is necessary, because it provides those RecyclerViewActions, which do not support assertions.

import android.support.test.espresso.contrib.RecyclerViewActions;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;

@RunWith(AndroidJUnit4.class)
public class TestIngredients {

    /** the Activity of the Target application */
    private IngredientsActivity mActivity;

    /** the {@link RecyclerView}'s resource id */
    private int resId = R.id.recyclerview_ingredients;

    /** the {@link RecyclerView} */
    private IngredientsLinearView mRecyclerView;

    /** and it's item count */
    private int itemCount = 0;

    /**
     * such a {@link ActivityTestRule} can be used eg. for Intent.putExtra(),
     * alike one would pass command-line arguments to regular run configurations.
     * this code runs before the {@link FragmentActivity} is being started.
     * there also would be an {@link IntentsTestRule}, but not required here.
    **/
    @Rule
    public ActivityTestRule<IngredientsActivity> mActivityRule = new ActivityTestRule<IngredientsActivity>(IngredientsActivity.class) {

        @Override
        protected Intent getActivityIntent() {
            Intent intent = new Intent();
            Bundle extras = new Bundle();
            intent.putExtras(extras);
            return intent;
        }
    };

    @Before
    public void setUpTest() {

        /* obtaining the Activity from the ActivityTestRule */
        this.mActivity = this.mActivityRule.getActivity();

        /* obtaining handles to the Ui of the Activity */
        this.mRecyclerView = this.mActivity.findViewById(this.resId);
        this.itemCount = this.mRecyclerView.getAdapter().getItemCount();
    }

    @Test
    public void RecyclerViewTest() {
        if(this.itemCount > 0) {
            for(int i=0; i < this.itemCount; i++) {

                /* clicking the item */
                onView(withId(this.resId))
                  .perform(RecyclerViewActions.actionOnItemAtPosition(i, click()));

                /* check if the ViewHolder is being displayed */
                onView(new RecyclerViewMatcher(this.resId)
                  .atPositionOnView(i, R.id.cardview))
                  .check(matches(isDisplayed()));

                /* checking for the text of the first one item */
                if(i == 0) {
                    onView(new RecyclerViewMatcher(this.resId)
                      .atPositionOnView(i, R.id.ingredientName))
                      .check(matches(withText("Farbstoffe")));
                }

            }
        }
    }
}

Instead one can use a RecyclerViewMatcher for that:

public class RecyclerViewMatcher {

    private final int recyclerViewId;

    public RecyclerViewMatcher(int recyclerViewId) {
        this.recyclerViewId = recyclerViewId;
    }

    public Matcher<View> atPosition(final int position) {
        return atPositionOnView(position, -1);
    }

    public Matcher<View> atPositionOnView(final int position, final int targetViewId) {
        return new TypeSafeMatcher<View>() {
            Resources resources = null;
            View childView;
            public void describeTo(Description description) {
                String idDescription = Integer.toString(recyclerViewId);
                if(this.resources != null) {
                    try {
                        idDescription = this.resources.getResourceName(recyclerViewId);
                    } catch (Resources.NotFoundException var4) {
                        idDescription = String.format("%s (resource name not found)",
                        new Object[] {Integer.valueOf(recyclerViewId) });
                    }
                }
                description.appendText("with id: " + idDescription);
            }

            public boolean matchesSafely(View view) {
                this.resources = view.getResources();
                if (childView == null) {
                    RecyclerView recyclerView = (RecyclerView) view.getRootView().findViewById(recyclerViewId);
                    if (recyclerView != null && recyclerView.getId() == recyclerViewId) {
                        childView = recyclerView.findViewHolderForAdapterPosition(position).itemView;
                    } else {
                        return false;
                    }
                }
                if (targetViewId == -1) {
                    return view == childView;
                } else {
                    View targetView = childView.findViewById(targetViewId);
                    return view == targetView;
                }
            }
        };
    }
}

screen recorder

Solution 2

RecyclerViewMatcher from @Martin Zeitler's answer with more informative error reporting.

import android.view.View;

import android.content.res.Resources;
import androidx.recyclerview.widget.RecyclerView;

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;

import static com.google.common.base.Preconditions.checkState;

public class RecyclerViewMatcher {

    public static final int UNSPECIFIED = -1;
    private final int recyclerId;

    public RecyclerViewMatcher(int recyclerViewId) {
        this.recyclerId = recyclerViewId;
    }

    public Matcher<View> atPosition(final int position) {
        return atPositionOnView(position, UNSPECIFIED);
    }

    public Matcher<View> atPositionOnView(final int position, final int targetViewId) {
        return new TypeSafeMatcher<View>() {
            Resources resources;
            RecyclerView recycler;
            RecyclerView.ViewHolder holder;

            @Override
            public void describeTo(Description description) {
                checkState(resources != null, "resource should be init by matchesSafely()");

                if (recycler == null) {
                    description.appendText("RecyclerView with " + getResourceName(recyclerId));
                    return;
                }

                if (holder == null) {
                    description.appendText(String.format(
                            "in RecyclerView (%s) at position %s",
                            getResourceName(recyclerId), position));
                    return;
                }

                if (targetViewId == UNSPECIFIED) {
                    description.appendText(
                            String.format("in RecyclerView (%s) at position %s",
                            getResourceName(recyclerId), position));
                    return;
                }

                description.appendText(
                        String.format("in RecyclerView (%s) at position %s and with %s",
                                getResourceName(recyclerId),
                                position,
                                getResourceName(targetViewId)));
            }

            private String getResourceName(int id) {
                try {
                    return "R.id." + resources.getResourceEntryName(id);
                } catch (Resources.NotFoundException ex) {
                    return String.format("resource id %s - name not found", id);
                }
            }

            @Override
            public boolean matchesSafely(View view) {
                resources = view.getResources();
                recycler = view.getRootView().findViewById(recyclerId);
                if (recycler == null)
                    return false;
                holder = recycler.findViewHolderForAdapterPosition(position);
                if (holder == null)
                    return false;

                if (targetViewId == UNSPECIFIED) {
                    return view == holder.itemView;
                } else {
                    return view == holder.itemView.findViewById(targetViewId);
                }
            }
        };
    }
}
Share:
10,861

Related videos on Youtube

tccpg288
Author by

tccpg288

I quit my corporate job as a certified public accountant to learn computer programming. I am starting with Java / Android and eventually going to work to Shift / iOS. Currently, I have 2 applications published in the Google Play Store. I am maintaining these applications while finding ways to build new applications at the same time. Right now, I am starting to learn server-side programming, specifically Retrofit and Google App Engine Servlets. Let me know if you have any advice or can offer input!

Updated on September 14, 2022

Comments

  • tccpg288
    tccpg288 over 1 year

    I want to test the text contained in each ViewHolder of my RecyclerView:

    @RunWith(AndroidJUnit4.class)
    public class EspressoTest {
    
        private Activity mMainActivity;
        private RecyclerView mRecyclerView;
        private int res_ID = R.id.recycler_view_ingredients;
        private int itemCount = 0;
    
        //TODO: What is the purpose of this rule as it relates to the Test below?
        @Rule
        public ActivityTestRule<MainActivity> firstRule = new ActivityTestRule<>(MainActivity.class);
    
    
        //TODO: Very confused about Espresso testing and the dependencies required; it appears Recyclerview
        //TODO: Requires additional dependencies other than those mentioned in the Android documentation?
        //TODO: What would be best method of testing all views of RecyclerView? What is there is a dynamic number of Views that are populated in RecyclerView?
    
    
        //TODO: Instruction from StackOverflow Post: https://stackoverflow.com/questions/51678563/how-to-test-recyclerview-viewholder-text-with-espresso/51698252?noredirect=1#comment90433415_51698252
        //TODO: Is this necessary?
        @Before
        public void setupTest() {
            this.mMainActivity = this.firstRule.getActivity();
            this.mRecyclerView = this.mMainActivity.findViewById(this.res_ID);
            this.itemCount = this.mRecyclerView.getAdapter().getItemCount();
    
        }
    
        @Test
        public void testRecyclerViewClick() {
            Espresso.onView(ViewMatchers.withId(R.id.recycler_view_ingredients)).perform(RecyclerViewActions.actionOnItemAtPosition(1, ViewActions.click()));
        }
    
        //CANNOT CALL THIS METHOD, THE DEPENDENCIES ARE INCORRECT
        @Test
        public void testRecyclerViewText() {
            // Check item at position 3 has "Some content"
            onView(withRecyclerView(R.id.scroll_view).atPosition(3))
                    .check(matches(hasDescendant(withText("Some content"))));
    
    
            }
         }
    }
    

    Below is my gradle as well, I never understood what separate dependencies are required for RecyclerView testing:

    dependencies {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
        implementation 'com.android.support:recyclerview-v7:27.1.1'
        implementation 'com.google.android.exoplayer:exoplayer:2.6.1'
        implementation 'com.android.support:appcompat-v7:27.1.1'
        implementation 'com.android.support.constraint:constraint-layout:1.1.2'
        implementation 'com.android.support:support-v4:27.1.1'
        testImplementation 'junit:junit:4.12'
        androidTestImplementation 'com.android.support.test:testing-support-lib:0.1'
        androidTestImplementation 'com.android.support.test.espresso:espresso-core:2.0'
        androidTestImplementation('com.android.support.test.espresso:espresso-contrib:2.0') {
            exclude group: 'com.android.support', module: 'appcompat'
            exclude group: 'com.android.support', module: 'support-v4'
            exclude module: 'recyclerview-v7'
        }
        implementation 'com.android.support:support-annotations:27.1.1'
        implementation 'com.squareup.okhttp3:okhttp:3.10.0'
        implementation 'com.google.code.gson:gson:2.8.2'
        implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
        implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'
        androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
        implementation 'com.android.support:cardview-v7:27.1.1'
        implementation 'com.google.android.exoplayer:exoplayer:2.6.0'
    }
    

    Also, what if the RecyclerView populates data dynamically? Then you simply could not hard code the position you wanted to test....

    • Martin Zeitler
      Martin Zeitler almost 6 years
      use mRecyclerView.getAdapter().getItemCount(), in order to limit the loop.
  • tccpg288
    tccpg288 over 5 years
    Thanks Bro really appreciate your explicit response
  • Martin Zeitler
    Martin Zeitler over 5 years
    @tccpg288 also can use the code; have noticed that a longClick() on items, with a context-menu appear to be more tricky, because one has to obtain a handle to the menu's adapter then; and also just closing that menu seems to be more difficult than one would imagine.
  • tccpg288
    tccpg288 over 5 years
    where does IngredientsLinearView come from? Not understanding that ViewType
  • Martin Zeitler
    Martin Zeitler over 5 years
    it's just a RecyclerView, with a LinearLayoutManager (eg. in order to tell it apart from the IngredientsGridView), which can be substituted with just any RecyclerView, with whatever RecyclerView.LayoutManager... the Javadoc markup above the declaration tells, that it is supposed to be a RecyclerView
  • tccpg288
    tccpg288 over 5 years
    I updated my question, not sure if you're logic still applies. I cannot access RecyclerViewMatcher
  • Martin Zeitler
    Martin Zeitler over 5 years
    @tccpg288 the answer had the link to that class; now I've even added it here. and please accept the answer, because it a) clicks each single item and b) is able to access the text of each single item.
  • Martin Zeitler
    Martin Zeitler over 5 years
    @tccpg288 now even added a GIF. and it does not matter if this is populated statically from an array resource or dynamically from a database... comparing the text of the TextView might be pointless, because one can probably assume, that the same query (or the same array index) might deliver the same result. accessibility is rather relevant (eg. bottom item not being covered by a toolbar).
  • tccpg288
    tccpg288 over 5 years
    So you are creating a custom class? RecylcerViewMatcher?
  • tccpg288
    tccpg288 over 5 years
    Also, very confused, are you still using the dependency for rules and annotations? it does not appear to be included in the GitHub repo
  • Martin Zeitler
    Martin Zeitler over 5 years
    @tccpg288 obviously the RecylcerViewMatcher class is not only being declared, but being used to get a handle to the item views. the GitHub link goes straight to that class, which also is also included here and the other dependency is espresso-contrib, as already stated in the answer.
  • tccpg288
    tccpg288 over 5 years
    It tried the custom solution. It appears there are additional dependencies needed that were not included in github. I saw references to hamcrest dependencies that were not listed in the build.gradle of the github?
  • tccpg288
    tccpg288 over 5 years
    I was not able to import Matcher and TypeSafeMatcher
  • Martin Zeitler
    Martin Zeitler over 5 years
    @tccpg288 they're in package org.hamcrest. the IDE ordinary should suggest most of these; eg. when clicking onto red text, then hitting <Alt> + <Enter>.
  • tccpg288
    tccpg288 over 5 years
    Yes I am typically able to import, but in this case it did not happen
  • Martin Zeitler
    Martin Zeitler over 5 years
    @tccpg288 added the relevant dependencies to the code. you may want to remove the com.android.support.test:testing-support-lib and update espresso to 3.0.2, and the test-rules & test-runner to 1.0.2.
  • tccpg288
    tccpg288 over 5 years
    appreciate your help, can you post your full gradle dependencies? I am confused why I am calling Espresso.onView() whereas you are simply calling onView()
  • Martin Zeitler
    Martin Zeitler over 5 years
    @tccpg288 I've already added those dependencies, see those import static.