RecyclerView Espresso 测试由于 RunTimeException 而失败



我正在使用以下代码,试图设置浓缩咖啡:

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

它不会成功运行,我不明白为什么。下面是错误:

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)

完整的 Github 存储库: https://github.com/troy21688/KitchenPal

编辑:测试实际上通过了模拟器,但没有通过我的实际手机(Google Nexus 6(。这让我相信这与屏幕尺寸在每个设备上的渲染方式有关。

ID 为recycler_view_ingredientsRecyclerView的高度为wrap_content,因此当它没有子项或适配器为空时,高度将为 0。该错误表示不会执行操作,因为目标视图RecyclerView未显示(height=0(,这也意味着当时尚未加载数据。

你的应用在不同的线程上异步加载数据,然后在完全加载后更新主线程上的RecyclerView。作为一个问题 事实上,Espresso 只在主线程上同步,所以当你的应用开始在后台加载数据时,它会认为应用的主线程已经空闲,所以它继续执行操作,这可能会失败,也可能不会失败取决于设备性能。

解决此问题的一种简单方法是添加一些延迟,例如第二个:

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

或者,修复它的一种优雅方法是使用IdlingResource

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

以下是一些免费课程:

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

当您的测试在一个设备中工作而在另一个 %90 的时间内失败时,这是因为同步问题(您的测试尝试在网络调用完成之前执行断言/操作(,%9 的时间是因为您需要在某些设备中滚动视图,因为屏幕尺寸不同。虽然 Aaron 的解决方案可能有效,但很难将 Idling 资源用于大型项目,并且空闲资源使您的测试每次等待 5 秒。这是一种更简单的方法,可以在每种可能的情况下等待您的匹配器成功

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
}

你可以像这样使用它

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

最新更新