espresso
更新数据时,使用Android Espresso测试RecyclerView时遇到问题。
这是针对Android应用程序,其中RecyclerView显示联系人列表。 操作栏中有一个SearchView ,可以过滤联系人列表以显示匹配的联系人姓名。
Espresso测试运行如下:
- 开始活动。
- Espresso验证联系人的完整列表是否显示在RecyclerView中。 这很好。
- 在SearchView中输入查询字符串,然后启动RecyclerView中的数据过滤(我使用SearchView来获取查询字符串,但是可以使用诸如EditText等其他控件代替)。
- Espresso验证联系人列表已更改为仅显示匹配项。 失败
@RunWith (AndroidJUnit4. class ) public class RecyclerViewIdlingResourceTest { @Rule public ActivityRule<MainActivity> activityRule = ActivityRule<MainActivity> activityRule = new ActivityRule<MainActivity>(MainActivity. class ); // number of items in the original list int allItemsCount = ...; // number of items after the list has been filtered int filteredItemsCount = ...; @Test public void testRecyclerviewFilter() {
// verify all test items loaded
// SUCCESS
onView(withId(R.id.recyclerview)).check(withItemCount(allItemsCount));
// since the search view is initially collapsed, open it first before tests are run
onView(withId(R.id.action_search)).perform(click());
// enter some text into the search view, and then press the action button.
String searchText = "test"
onView(withId(android.support.design.R.id.search_src_text)).perform(typeText(searchText), pressImeActionButton());
// verify the number of items in the recyclerview list has been altered
// FAIL!
onView(withId(R.id.recyclerview)).check(withItemCount(filteredItemsCount)); } }
不幸的是,似乎Espresso断言要验证项目列表是否已更改,发生在RecyclerView完成重新加载更新的数据并重新绘制自身之前。 因此,当发现RecyclerView仍然具有原始项目数时,该测试将失败,因为它尚未使用新的数据列表重新绘制自身。
这篇文章的代码在本要点中 。 它采用不完整代码的形式,仅包含与帖子相关的内容。 另外,还有多种实现和过滤RecyclerView的方法,因此我将把这一部分留给读者。
所以有什么问题?
更改RecyclerView的数据后,我在适配器上调用notifyDataSetChanged() 。
基于这个StackOverflow问题 ,问题似乎是当调用notifyDataSetChanged()时,它仅使RecyclerView中的数据无效,而没有立即更新该小部件。 因此,我怀疑Espresso断言是在RecyclerView更新之前发生的。
为了对此进行测试,我在Espresso断言之前引入了一个暂停,以允许RecyclerView有时间更新,并且测试通过了。
@Test public void testRecyclerviewFilterWithPause() {
// verify all test items loaded
// SUCCESS
onView(withId(R.id.recyclerview)).check(withItemCount(allItemsCount));
// since the search view is initially collapsed, open it first before tests are run
onView(withId(R.id.action_search)).perform(click());
// enter some text into the search view, and then press the action button.
String searchText = "test"
onView(withId(android.support.design.R.id.search_src_text)).perform(typeText(searchText), pressImeActionButton());
// pause for arbitrary period of time, InterruptedException handling left out to simplify example
Thread.sleep( 1000 );
// verify the number of items in the recyclerview list has been altered
// SUCCESS - assuming the pause time was long enough
onView(withId(R.id.recyclerview)).check(withItemCount(filteredItemsCount)); }
在这里,我使用的是Thread.sleep(),但是任何具有Handlers的Android等都可以做到。 当然,这全都是小技巧。 建议在Espresso继续测试之前引入等待某些过程完成的方法是使用Idling Resources 。
在某个任意时间段使用暂停并不理想,因为它经常导致不稳定的测试或使测试的运行时间超出必要。
RecyclerView回调
为了使空闲资源正常工作,它需要知道RecyclerView RecyclerView何时使用新的列表数据进行重绘。
RecyclerView(及其支持类)具有各种回调,这些回调可以表明RecyclerView正在重绘过程中。 在StackOverflow上搜索之后,我发现了以下可能性:
- https://stackoverflow.com/questions/30397460/how-to-know-when-the-recyclerview-has-finished-laying-down-the-items
- https://stackoverflow.com/questions/7517636/viewgroup-finish-inflate-event
- https://stackoverflow.com/questions/32678632/is-there-a-callback-for-when-recyclerview-has-finished-showing-its-items-after-i
我决定使用onGlobalLayoutListener ,但是RecyclerView似乎有多种方式来表示它已被重绘。
空闲资源正在侦听...
首先,我们需要一些接口用作回调,以在RecyclerView,包含RecyclerView和空闲资源的活动/片段之间进行通信。
首先,这里是RecyclerView的接口,用于在使用新数据进行重绘过程时通知活动。
public interface RecyclerViewIdlingCallback { public void setRecyclerViewLayoutCompleteListener(RecyclerViewLayoutCompleteListener listener); public void removeRecyclerViewLayoutCompleteListener(RecyclerViewLayoutCompleteListener listener); // Callback for the idling resource to check if the resource (in this example the activity containing the recyclerview) // is idle public boolean isRecyclerViewLayoutCompleted(); }
然后,另一个接口用作活动的回调,以将空闲资源通知..空闲。
public interface RecyclerViewLayoutCompleteListener { // Callback to notify the idling resource that it can transition to the idle state public void onLayoutCompleted(); }
这是一个示例活动,仅显示与空闲资源一起使用的相关代码。
public class RecyclerViewCallbackContactsActivity extends AppCompatActivity implements
SearchView.OnQueryTextListener,
ViewTreeObserver.OnGlobalLayoutListener,
RecyclerViewIdlingCallback {
/**
* Flag to indicate if the layout for the recyclerview has complete. This should only be used
* when the data in the recyclerview has been changed after the initial loading.
*/
private boolean recyclerViewLayoutCompleted;
/**
* Listener to be set by the idling resource, so that it can be notified when recyclerview
* layout has been done.
*/
private RecyclerViewLayoutCompleteListener listener;
@Override
public void onCreate (Bundle savedInstanceState) {
super .onCreate(savedInstanceState);
// CODE HERE to initialize the recyclerview
recyclerViewLayoutCompleted = true ;
recyclerView.getViewTreeObserver().addOnGlobalLayoutListener( this );
}
@Override
public boolean onQueryTextSubmit(String query) {
// CODE HERE to filter the recyclerview using the query string,
// - this should eventually result in notifyDataSetChanged() being called on the adapter
// flag that a new layout will be required with the filtered data
recyclerViewLayoutCompleted = false ;
}
@Override
public void onGlobalLayout() {
if (listener != null )
{
// set flag to let the idling resource know that processing has completed and is now idle
recyclerViewLayoutCompleted = true ;
// notify the listener (should be in the idling resource)
listener.onLayoutCompleted();
}
}
@Override
public boolean isRecyclerViewLayoutCompleted() {
return recyclerViewLayoutCompleted;
}
@Override
public void setRecyclerViewLayoutCompleteListener(RecyclerViewLayoutCompleteListener listener) {
this .listener = listener;
}
@Override
public void removeRecyclerViewLayoutCompleteListener(RecyclerViewLayoutCompleteListener listener) {
if ( this .listener != null && this .listener == listener)
{
this .listener = null ;
}
} }
该活动的重要部分是:
- 侦听器方法onGlobalLayout(),该信号指示recyclerview夸大了其布局以进行重绘
- 布尔标志recyclerViewLayoutCompleted ,由空闲资源使用,以检查recyclerview重绘后Espresso测试是否可以继续运行。
这是用于在活动中测试recyclerview的空闲资源。
public class RecyclerViewLayoutCompleteIdlingResource implements IdlingResource {
private ResourceCallback resourceCallback;
private RecyclerViewIdlingCallback recyclerViewIdlingCallback;
private RecyclerViewLayoutCompleteListener listener;
public RecyclerViewLayoutCompleteIdlingResource(RecyclerViewIdlingCallback recyclerViewIdlingCallback){
this .recyclerViewIdlingCallback = recyclerViewIdlingCallback;
listener = new RecyclerViewLayoutCompleteListener() {
@Override
public void onLayoutCompleted() {
if (resourceCallback == null ){
return ;
}
if (listener != null ) {
recyclerViewIdlingCallback.removeRecyclerViewLayoutCompleteListener(listener);
}
//Called when the resource goes from busy to idle.
resourceCallback.onTransitionToIdle();
}
};
// add the listener to the view containing the recyclerview
recyclerViewIdlingCallback.setRecyclerViewLayoutCompleteListener (listener);
}
@Override
public String getName() {
return "RecyclerViewLayoutCompleteIdlingResource" ;
}
@Override
public boolean isIdleNow() {
return recyclerViewIdlingCallback.isRecyclerViewLayoutCompleted();
}
@Override
public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
this .resourceCallback = resourceCallback;
} }
该活动作为RecyclerViewIdlingCallback实现传递到其构造函数中的空闲资源。 然后,当活动中的recyclerview准备就绪时,活动将在空闲资源中调用回调以指示它为“空闲”。
最后,我们可以将其合并到Espresso测试中。
@Test public void testFilterRecyclerViewUsingSearchView() {
// CODE HERE use espresso to use the SearchView to filter the recyclerview
RecyclerViewLayoutCompleteIdlingResource idlingResource = new RecyclerViewLayoutCompleteIdlingResource((RecyclerViewCallbackContactsActivity) activityTestRule.getActivity());
IdlingRegistry.getInstance().register(idlingResource);
// CODE HERE to verify the recyclerview with the updated data
IdlingRegistry.getInstance().unregister(idlingResource); }
警告
我实际上是前一段时间开始写这篇文章的,所以代码是从Android支持库而不是从Androidx库用于Recyclerview的 。
翻译自: https://www.javacodegeeks.com/2019/08/espresso-idling-resource-recyclerview-data-changes.html
espresso