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....
To interact with RecyclerViews using Espresso, you can use the espresso-contrib package, which has a collection of RecyclerViewActions that can be used to scroll to positions or to perform actions on items: scrollTo() - Scrolls to the matched View, if it exists.
The basic idea is to use a method with an internal ViewAction that retrieves the text in its perform method. Anonymous classes can only access final fields, so we cannot just let it set a local variable of getText() , but instead an array of String is used to get the string out of the ViewAction .
A RecyclerView. ViewHolder class which caches views associated with the default Preference layouts. A ViewHolder describes an item view and metadata about its place within the RecyclerView. Adapter implementations should subclass ViewHolder and add fields for caching potentially expensive findViewById results.
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;
}
}
};
}
}
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);
}
}
};
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With