问题
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