Как получить доступ к RecyclerView ViewHolder с помощью эспрессо?

Я хочу протестировать текст, содержащийся в каждом 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 заполняет данные динамически? Тогда вы просто не смогли бы жестко закодировать позицию, которую хотите проверить ...

используйте mRecyclerView.getAdapter().getItemCount(), чтобы ограничить цикл.

Martin Zeitler 05.08.2018 19:34

какой код я должен написать в моем методе .testRecyclerViewText()?

tccpg288 05.08.2018 20:28
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
6
2
8 937
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Пакет эспрессо 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;
                }
            }
        };
    }
}

screen recorder

Спасибо, брат, очень признателен за твой явный ответ

tccpg288 08.08.2018 00:34

@ tccpg288 также может использовать код; заметили, что longClick() на элементах с контекстным меню кажется более сложным, потому что тогда нужно получить дескриптор адаптера меню; а также просто закрыть это меню, кажется, сложнее, чем можно себе представить.

Martin Zeitler 08.08.2018 00:41

откуда появился IngredientsLinearView? Не понимая, что ViewType

tccpg288 09.08.2018 03:29

это просто RecyclerView с LinearLayoutManager (например, чтобы отличить его от IngredientsGridView), который можно заменить любым RecyclerView, любым RecyclerView.LayoutManager ... разметка Javadoc над объявлением говорит, что он должен быть RecyclerView

Martin Zeitler 09.08.2018 12:56

Я обновил свой вопрос, не уверен, что ваша логика все еще применима. Я не могу получить доступ к RecyclerViewMatcher

tccpg288 11.08.2018 21:57

@ tccpg288 в ответе была ссылка на этот класс; теперь я даже добавил это сюда. и, пожалуйста, примите ответ, потому что он а) щелкает каждый отдельный элемент и б) может получить доступ к тексту каждого отдельного элемента.

Martin Zeitler 11.08.2018 22:24

@ tccpg288 теперь даже GIF добавил. и не имеет значения, заполняется ли он статически из ресурса массива или динамически из базы данных ... сравнение текста TextView может быть бессмысленным, потому что можно предположить, что тот же запрос (или тот же индекс массива) может доставить тот же результат. доступность весьма актуальна (например, нижний элемент не перекрывается панелью инструментов).

Martin Zeitler 13.08.2018 08:51

Итак, вы создаете собственный класс? RecylcerViewMatcher?

tccpg288 15.08.2018 02:21

Кроме того, очень смущен, вы все еще используете зависимость для правил и аннотаций? не похоже, что он включен в репозиторий GitHub

tccpg288 15.08.2018 02:28

@ tccpg288 очевидно, что класс RecylcerViewMatcher не только объявляется, но и используется для получения дескриптора представлений элементов. ссылка GitHub ведет прямо к этому классу, который также включен сюда, а другая зависимость - espresso-contrib, как уже было сказано в ответе.

Martin Zeitler 15.08.2018 03:05

Он попробовал нестандартное решение. Похоже, требуются дополнительные зависимости, которые не были включены в github. Я видел ссылки на зависимости hamcrest, которые не были перечислены в build.gradle на github?

tccpg288 15.08.2018 03:18

Мне не удалось импортировать Matcher и TypeSafeMatcher

tccpg288 15.08.2018 03:21

@ tccpg288 они находятся в пакете org.hamcrest. обычная среда IDE должна предлагать большинство из них; например. при нажатии на красный текст, затем при нажатии на <Alt> + <Enter>.

Martin Zeitler 15.08.2018 07:43

Да, я обычно могу импортировать, но в этом случае этого не произошло

tccpg288 15.08.2018 14:06

@ tccpg288 добавил в код соответствующие зависимости. вы можете удалить com.android.support.test:testing-support-lib и обновить espresso до 3.0.2, а правила тестирования и средство запуска тестов до 1.0.2.

Martin Zeitler 15.08.2018 16:07

цените вашу помощь, можете ли вы опубликовать свои полные зависимости Gradle? Я не понимаю, почему я вызываю Espresso.onView (), тогда как вы просто вызываете onView ()

tccpg288 18.08.2018 20:33

@ tccpg288 Я уже добавил эти зависимости, посмотрите эти import static.

Martin Zeitler 19.08.2018 02:03

Вы можете легко получить доступ к 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);
                }
            }
        };
    }
}

Другие вопросы по теме