Espresso因同步问题报AmbiguousViewMatcherException,测试偶发失败如何解决?
根因分析
你遇到的竞态问题确实和异步操作有关,但不是ActivityScenario.onActivity的非阻塞导致的:onActivity的回调会同步在主线程执行,回调返回时你已经完成了列表数据的增删操作。你断点时界面表现正常,是因为断点暂停线程的时间足够长,让后续的异步逻辑执行完成,而正常运行时没有等待时间,就会触发问题:
问题出在RecyclerView的Item动画逻辑:你调用适配器的notifyItemRemoved等通知方法后,RecyclerView默认会执行Item退场动画,动画执行过程中,即将被移除的Item View仍然处于VISIBLE状态,还没有被从View树上移除。Espresso默认只会等待主线程普通任务队列空闲,不会等待属性动画执行完成,因此断言执行时会同时找到正在退场的text=1的Item和保留的text=12345的Item,触发AmbiguousViewMatcherException。
解决方案
方案1(最推荐):测试环境关闭RecyclerView Item动画
这是Espresso测试的通用最佳实践,从根源避免动画导致的异步不稳定问题。你可以在代码中加入测试环境判断,初始化RecyclerView时关闭动画:
// 在Activity初始化RecyclerView的位置添加 if (BuildConfig.TEST) { // 或者用其他测试环境判断逻辑 recyclerView.itemAnimator = null }
如果不想修改业务代码,也可以在测试用例中通过onActivity动态关闭:
@Before fun setup() { activityRule.scenario.onActivity { it.recyclerView.itemAnimator = null } }
方案2:优化匹配逻辑,过滤动画中的View
如果需要保留动画测试,可以修改匹配规则,只匹配当前真正可见、且符合内容要求的View,自动过滤掉处于退场动画的View:
onView(allOf( withId(R.id.item_text_id), withText("12345"), isDisplayed() )).check(matches(withText("12345")))
也可以使用通用的RecyclerView匹配辅助类,直接匹配RecyclerView指定位置的Item,避免匹配到多余的View:
// 匹配列表第0个Item中的文本控件 onView( withRecyclerView(R.id.your_recycler_view_id).atPosition(0) ).check(matches(hasDescendant(allOf(withId(R.id.item_text_id), withText("12345")))))
方案3:检查适配器的通知逻辑
确保你实现MutableCollection接口的增删方法时,调用的是精准的通知方法(如notifyItemRemoved/notifyItemInserted),而不是全局的notifyDataSetChanged,后者会导致整个列表重绘,延长异步等待时间,更容易触发竞态问题。
内容的提问来源于stack exchange,提问作者Kyle M

