RecyclerView Espresso Testing Fails Due to RunTimeException

删除回忆录丶 提交于 2020-07-10 06:43:23

问题


I have the following code I am using, trying to set up Espresso:

import android.support.test.espresso.Espresso;
import android.support.test.espresso.contrib.RecyclerViewActions;
import android.support.test.espresso.matcher.ViewMatchers;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import static android.support.test.espresso.action.ViewActions.click;

@RunWith(AndroidJUnit4.class)
public class EspressoTest {




    @Rule
    public ActivityTestRule<MainActivity> firstRule = new ActivityTestRule<>(MainActivity.class);




    @Test
    public void testRecyclerViewClick() {
        Espresso.onView(ViewMatchers.withId(R.id.recycler_view_ingredients)).perform(RecyclerViewActions.actionOnItemAtPosition(0, click()));




    }

}






    }

It will not run successfully and I do not understand why. Below is the error:

 Caused by: java.lang.RuntimeException: Action will not be performed because the target view does not match one or more of the following constraints:
(is assignable from class: class android.support.v7.widget.RecyclerView and is displayed on the screen to the user)
Target view: "RecyclerView{id=2131165335, res-name=recycler_view_ingredients, visibility=VISIBLE, width=1440, height=0, has-focus=true, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=true, is-focusable=true, is-layout-requested=false, is-selected=false, layout-params=android.support.constraint.ConstraintLayout$LayoutParams@caad301, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=0}"
at android.support.test.espresso.ViewInteraction.doPerform(ViewInteraction.java:245)
at android.support.test.espresso.ViewInteraction.access$100(ViewInteraction.java:63)
at android.support.test.espresso.ViewInteraction$1.call(ViewInteraction.java:153)
at android.support.test.espresso.ViewInteraction$1.call(ViewInteraction.java:150)

Full Github Repo: https://github.com/troy21688/KitchenPal

EDIT: The test actually passed on an emulator, but not my actual phone (Google Nexus 6). It leads me to believe it has something to do with how the screen size is being rendered on each device.


回答1:


Your RecyclerView with id recycler_view_ingredients has a height of wrap_content, so when it has no children or the adapter is empty, then the height will be 0. The error says that action will not be performed because the target view RecyclerView is not displayed (height=0), which also means that the data has not loaded yet at the time.

Your app is loading data asynchronously on different thread, then update your RecyclerView on the main thread when it has completely loaded. As a matter of fact, Espresso only synchronizes on main thread, so when your app starts to load data in the background, it thinks that the main thread of the app has gone idle, and so it proceeds to perform the action, which may or may not fail depends on devices performance.

An easy way to fix this issue is to add some delay, say a second:

Thread.sleep(1000);
onView(withId(R.id.recycler_view_ingredients)).perform(actionOnItemAtPosition(0, click()));

Or, an elegant way to fix it is to use IdlingResource:

onView(withId(R.id.recycler_view_ingredients))
    .perform(
        waitUntil(hasItemCount(greaterThan(0))), // wait until data has loaded
        actionOnItemAtPosition(0, click()));

And here are some complimentary classes:

public static Matcher<View> hasItemCount(Matcher<Integer> matcher) {
    return new BoundedMatcher<View, RecyclerView>(RecyclerView.class) {

        @Override public void describeTo(Description description) {
            description.appendText("has item count: ");
            matcher.describeTo(description);
        }

        @Override protected boolean matchesSafely(RecyclerView view) {
            return matcher.matches(view.getAdapter().getItemCount());
        }
    };
}

public static ViewAction waitUntil(Matcher<View> matcher) {
    return actionWithAssertions(new ViewAction() {
        @Override public Matcher<View> getConstraints() {
            return ViewMatchers.isAssignableFrom(View.class);
        }

        @Override public String getDescription() {
            StringDescription description = new StringDescription();
            matcher.describeTo(description);
            return String.format("wait until: %s", description);
        }

        @Override public void perform(UiController uiController, View view) {
            if (!matcher.matches(view)) {
                LayoutChangeCallback callback = new LayoutChangeCallback(matcher);
                try {
                    IdlingRegistry.getInstance().register(callback);
                    view.addOnLayoutChangeListener(callback);
                    uiController.loopMainThreadUntilIdle();
                } finally {
                    view.removeOnLayoutChangeListener(callback);
                    IdlingRegistry.getInstance().unregister(callback);
                }
            }
        }
    });
}

private static class LayoutChangeCallback implements IdlingResource, View.OnLayoutChangeListener {

    private Matcher<View> matcher;
    private IdlingResource.ResourceCallback callback;
    private boolean matched = false;

    LayoutChangeCallback(Matcher<View> matcher) {
        this.matcher = matcher;
    }

    @Override public String getName() {
        return "Layout change callback";
    }

    @Override public boolean isIdleNow() {
        return matched;
    }

    @Override public void registerIdleTransitionCallback(ResourceCallback callback) {
        this.callback = callback;
    }

    @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
        matched = matcher.matches(v);
        callback.onTransitionToIdle();
    }
}



回答2:


When your test works in one device and fails in another %90 of the time, it is because of synchronization issues (your test tries to do an assertion/action before network call completes) and %9 of the time it is because you need to scroll the view in some devices because screen sizes are different. While Aaron's solution may work it is very hard to use IdlingResources for big projects and idling resource make your tests wait 5 seconds each time it waits. Here is a simpler approach that waits for your matcher to succeed in every possible case

 fun waitUntilCondition(matcher: Matcher<View>, timeout: Long = DEFAULT_WAIT_TIMEOUT, condition: (View?) -> Boolean) {    

var success = false
        lateinit var exception: NoMatchingViewException
        val loopCount = timeout / DEFAULT_SLEEP_INTERVAL
        (0..loopCount).forEach {
            onView(matcher).check { view, noViewFoundException ->
                if (condition(view)) {
                    success = true
                    return@check
                } else {
                    Thread.sleep(DEFAULT_SLEEP_INTERVAL)
                    exception = noViewFoundException
                }
            }

            if (success) {
                return
            }
        }
        throw exception
    }

You can use it like

waitUntilCondition`(withId(id), timeout = 20000L) { it!= null}`


来源:https://stackoverflow.com/questions/51851653/recyclerview-espresso-testing-fails-due-to-runtimeexception

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!