Java 由于运行时异常,RecyclerView Espresso测试失败

Java 由于运行时异常,RecyclerView Espresso测试失败,java,android,android-recyclerview,android-espresso,Java,Android,Android Recyclerview,Android 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

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

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()));
    }
}
完整Github回购协议:


编辑:测试实际上通过了模拟器,但不是我的实际手机(谷歌Nexus6)。这让我相信它与每个设备上的屏幕大小呈现方式有关。

您的id为
recycler\u view\u Components的
recycler\u view\u高度为
wrap\u content
,因此当它没有子设备或适配器为空时,高度将为0。该错误表示将不执行操作,因为未显示目标视图
RecyclerView
height=0
),这也意味着此时尚未加载数据

您的应用程序正在不同线程上异步加载数据,然后在完全加载后更新主线程上的
RecyclerView
。事实上 事实上,Espresso只在主线程上进行同步,因此当你的应用程序开始在后台加载数据时,它会认为应用程序的主线程已处于空闲状态,因此它会继续执行该操作,该操作可能会失败,也可能不会失败,这取决于设备的性能

解决此问题的一个简单方法是增加一些延迟,例如秒:

Thread.sleep(1000);
onView(withId(R.id.recycler_view_ingredients)).perform(actionOnItemAtPosition(0, click()));
或者,一种优雅的修复方法是使用
idlingsource

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();
    }
}
公共静态匹配器hasItemCount(匹配器匹配器){
返回新的BoundedMatcher(RecyclerView.class){
@覆盖公共无效描述(描述){
description.appendText(“具有项目计数:”);
匹配器描述(描述);
}
@覆盖受保护的布尔匹配安全(RecyclerView视图){
返回matcher.matches(view.getAdapter().getItemCount());
}
};
}
公共静态ViewAction等待(匹配器匹配器){
返回带有断言的操作(new ViewAction(){
@重写公共匹配器getConstraints(){
返回ViewMatchers.isAssignableFrom(View.class);
}
@重写公共字符串getDescription(){
StringDescription description=新的StringDescription();
匹配器描述(描述);
返回String.format(“等待到:%s”,说明);
}
@覆盖公共作废执行(UiController UiController,视图){
如果(!matcher.matches(视图)){
LayoutChangeCallback=新的LayoutChangeCallback(匹配器);
试一试{
IdlingRegistry.getInstance().register(回调);
view.addOnLayoutChangeListener(回调);
uiController.loopMainThreadUntilIdle();
}最后{
view.removeOnLayoutChangeListener(回调);
IdlingRegistry.getInstance().unregister(回调);
}
}
}
});
}
私有静态类LayoutChangeCallback实现IdlingResource、View.OnLayoutChangeListener{
私人匹配器匹配器;
私有IdlingResource.ResourceCallback回调;
私有布尔匹配=假;
LayoutChangeCallback(匹配器匹配器){
this.matcher=matcher;
}
@重写公共字符串getName(){
返回“布局更改回调”;
}
@重写公共布尔值isIdleNow(){
返回匹配;
}
@重写公共无效RegisterIDletionCallback(ResourceCallback){
this.callback=回调;
}
@覆盖公共void onLayoutChange(视图v、int left、int top、int right、int bottom、int oldlight、int oldTop、int oldRight、int oldBottom){
匹配=匹配器。匹配(v);
callback.onTransitionToIdle();
}
}

当您的测试在一台设备上运行,并且在另外%90%的时间内失败时,这是因为同步问题(您的测试尝试在网络调用完成之前执行断言/操作),而%9%的时间是因为您需要在某些设备上滚动视图,因为屏幕大小不同。虽然Aaron的解决方案可能有效,但在大型项目中使用IdlingResources是非常困难的,而idlingResource使您的测试每次等待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
    }
你能看看这个吗?你能看看这个吗
 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}`