【问题标题】:How to access RecyclerView ViewHolder with Espresso?如何使用 Espresso 访问 RecyclerView ViewHolder?
【发布时间】:2020-10-07 20:54:12
【问题描述】:

我想测试我的RecyclerView 的每个ViewHolder 中包含的文本:

@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(),以限制循环。
  • 我应该在.testRecyclerViewText() 方法中写什么代码?

标签: java android android-recyclerview android-espresso


【解决方案1】:

Espresso 包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替换,任何@ 987654333@... 声明上方的Javadoc 标记告诉它应该是RecyclerView
  • @tccpg288 将相关依赖项添加到代码中。您可能想要删除 com.android.support.test:testing-support-lib 并将 espresso 更新为 3.0.2,并将 test-rules & test-runner 更新为 1.0.2
【解决方案2】:

RecyclerViewMatcher 来自 @Martin Zeitler's answer,提供更多信息的错误报告。

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);
                }
            }
        };
    }
}

【讨论】:

    【解决方案3】:

    您可以通过recyclerView模板轻松访问viewHolder onView(withId(R.id.your_list_id)).perform(actionOnItem&lt;RecyclerView.ViewHolder&gt;(withText(the_text_you_want), click()))

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-11-29
      • 2016-07-18
      • 1970-01-01
      相关资源
      最近更新 更多