How to access RecyclerView ViewHolder with Espresso?
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;
}
}
};
}
}
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);
}
}
};
}
}
Related videos on Youtube
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, 2022Comments
-
tccpg288 over 1 year
I want to test the text contained in each
ViewHolder
of myRecyclerView
:@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 almost 6 yearsuse
mRecyclerView.getAdapter().getItemCount()
, in order to limit the loop.
-
-
tccpg288 over 5 yearsThanks Bro really appreciate your explicit response
-
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 over 5 yearswhere does IngredientsLinearView come from? Not understanding that ViewType
-
Martin Zeitler over 5 yearsit's just a
RecyclerView
, with aLinearLayoutManager
(eg. in order to tell it apart from theIngredientsGridView
), which can be substituted with just anyRecyclerView
, with whateverRecyclerView.LayoutManager
... theJavadoc
markup above the declaration tells, that it is supposed to be aRecyclerView
-
tccpg288 over 5 yearsI updated my question, not sure if you're logic still applies. I cannot access RecyclerViewMatcher
-
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 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 over 5 yearsSo you are creating a custom class? RecylcerViewMatcher?
-
tccpg288 over 5 yearsAlso, 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 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 isespresso-contrib
, as already stated in the answer. -
tccpg288 over 5 yearsIt 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 over 5 yearsI was not able to import Matcher and TypeSafeMatcher
-
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 over 5 yearsYes I am typically able to import, but in this case it did not happen
-
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 to3.0.2
, and the test-rules & test-runner to1.0.2
. -
tccpg288 over 5 yearsappreciate 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 over 5 years@tccpg288 I've already added those dependencies, see those
import static
.