Я хочу протестировать текст, содержащийся в каждом ViewHolder моего 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"))));
}
}
}
Ниже также мой gradle, я никогда не понимал, какие отдельные зависимости требуются для тестирования RecyclerView:
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'
}
Кроме того, что, если RecyclerView заполняет данные динамически? Тогда вы просто не смогли бы жестко закодировать позицию, которую хотите проверить ...
какой код я должен написать в моем методе .testRecyclerViewText()?




Пакет эспрессо espresso-contrib необходим, потому что он предоставляет те RecyclerViewActions, которые не поддерживают утверждения.
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")));
}
}
}
}
}
Вместо этого для этого можно использовать RecyclerViewMatcher:
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;
}
}
};
}
}
Спасибо, брат, очень признателен за твой явный ответ
@ tccpg288 также может использовать код; заметили, что longClick() на элементах с контекстным меню кажется более сложным, потому что тогда нужно получить дескриптор адаптера меню; а также просто закрыть это меню, кажется, сложнее, чем можно себе представить.
откуда появился IngredientsLinearView? Не понимая, что ViewType
это просто RecyclerView с LinearLayoutManager (например, чтобы отличить его от IngredientsGridView), который можно заменить любым RecyclerView, любым RecyclerView.LayoutManager ... разметка Javadoc над объявлением говорит, что он должен быть RecyclerView
Я обновил свой вопрос, не уверен, что ваша логика все еще применима. Я не могу получить доступ к RecyclerViewMatcher
@ tccpg288 в ответе была ссылка на этот класс; теперь я даже добавил это сюда. и, пожалуйста, примите ответ, потому что он а) щелкает каждый отдельный элемент и б) может получить доступ к тексту каждого отдельного элемента.
@ tccpg288 теперь даже GIF добавил. и не имеет значения, заполняется ли он статически из ресурса массива или динамически из базы данных ... сравнение текста TextView может быть бессмысленным, потому что можно предположить, что тот же запрос (или тот же индекс массива) может доставить тот же результат. доступность весьма актуальна (например, нижний элемент не перекрывается панелью инструментов).
Итак, вы создаете собственный класс? RecylcerViewMatcher?
Кроме того, очень смущен, вы все еще используете зависимость для правил и аннотаций? не похоже, что он включен в репозиторий GitHub
@ tccpg288 очевидно, что класс RecylcerViewMatcher не только объявляется, но и используется для получения дескриптора представлений элементов. ссылка GitHub ведет прямо к этому классу, который также включен сюда, а другая зависимость - espresso-contrib, как уже было сказано в ответе.
Он попробовал нестандартное решение. Похоже, требуются дополнительные зависимости, которые не были включены в github. Я видел ссылки на зависимости hamcrest, которые не были перечислены в build.gradle на github?
Мне не удалось импортировать Matcher и TypeSafeMatcher
@ tccpg288 они находятся в пакете org.hamcrest. обычная среда IDE должна предлагать большинство из них; например. при нажатии на красный текст, затем при нажатии на <Alt> + <Enter>.
Да, я обычно могу импортировать, но в этом случае этого не произошло
@ tccpg288 добавил в код соответствующие зависимости. вы можете удалить com.android.support.test:testing-support-lib и обновить espresso до 3.0.2, а правила тестирования и средство запуска тестов до 1.0.2.
цените вашу помощь, можете ли вы опубликовать свои полные зависимости Gradle? Я не понимаю, почему я вызываю Espresso.onView (), тогда как вы просто вызываете onView ()
@ tccpg288 Я уже добавил эти зависимости, посмотрите эти import static.
Вы можете легко получить доступ к viewHolder с помощью шаблона recyclerViewonView(withId(R.id.your_list_id)).perform(actionOnItem<RecyclerView.ViewHolder>(withText(the_text_you_want), click()))
RecyclerViewMatcher от Ответ @Martin Zeitler с более информативным сообщением об ошибках.
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);
}
}
};
}
}
используйте
mRecyclerView.getAdapter().getItemCount(), чтобы ограничить цикл.