У меня возникла проблема с использованием Android Espresso для тестирования RecyclerView при обновлении данных.
Это для приложения Android, где RecyclerView отображает список контактов. В панели действий есть SearchView, который может фильтровать список контактов для отображения совпадающих имен контактов.
Тест эспрессо проходил так:
- Начните деятельность.
- Эспрессо проверяет, что полный список контактов отображается в RecyclerView. Это отлично работает.
- Строка запроса вводится в SearchView, и инициируется фильтрация данных в RecyclerView (я использую SearchView для получения строки запроса, но вместо этого можно использовать другие элементы управления, такие как EditText и т. Д.).
- Эспрессо проверяет, что список контактов изменился, чтобы отображать только соответствующие элементы. Не удается.
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | @RunWith(AndroidJUnit4.class)publicclassRecyclerViewIdlingResourceTest {@RulepublicActivityRule<MainActivity> activityRule = newActivityRule<MainActivity>(MainActivity.class);// number of items in the original listintallItemsCount = ...;// number of items after the list has been filteredintfilteredItemsCount = ...;@TestpublicvoidtestRecyclerviewFilter(){  // 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 и прохождения теста.
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 | @TestpublicvoidtestRecyclerviewFilterWithPause(){  // 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 (), но любой 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 может сигнализировать о перерисовке несколькими способами.
The Idling Resource слушает в …
Во-первых, нам нужны некоторые интерфейсы для использования в качестве обратных вызовов для связи между RecyclerView, действием / фрагментом, содержащим RecyclerView и Idling Resource.
Во-первых, это интерфейс для RecyclerView, который уведомляет об активности, когда происходит процесс перерисовки с новыми данными.
| 01 02 03 04 05 06 07 08 09 10 | publicinterfaceRecyclerViewIdlingCallback {publicvoidsetRecyclerViewLayoutCompleteListener(RecyclerViewLayoutCompleteListener listener);publicvoidremoveRecyclerViewLayoutCompleteListener(RecyclerViewLayoutCompleteListener listener);// Callback for the idling resource to check if the resource (in this example the activity containing the recyclerview)// is idlepublicbooleanisRecyclerViewLayoutCompleted();} | 
Затем другой интерфейс для использования в качестве обратного вызова для действия, чтобы уведомить Ресурс бездействия к … простоя.
| 1 2 3 4 5 | publicinterfaceRecyclerViewLayoutCompleteListener {// Callback to notify the idling resource that it can transition to the idle statepublicvoidonLayoutCompleted();} | 
Вот пример действия, показывающий только соответствующий код для работы с ресурсом холостого хода.
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | publicclassRecyclerViewCallbackContactsActivity extendsAppCompatActivity 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.   */  privatebooleanrecyclerViewLayoutCompleted;  /**   * Listener to be set by the idling resource, so that it can be notified when recyclerview   * layout has been done.   */  privateRecyclerViewLayoutCompleteListener listener;  @Override  publicvoidonCreate (Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    // CODE HERE to initialize the recyclerview    recyclerViewLayoutCompleted = true;    recyclerView.getViewTreeObserver().addOnGlobalLayoutListener(this);  }  @Override  publicbooleanonQueryTextSubmit(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  publicvoidonGlobalLayout() {    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  publicbooleanisRecyclerViewLayoutCompleted() {    returnrecyclerViewLayoutCompleted;  }  @Override  publicvoidsetRecyclerViewLayoutCompleteListener(RecyclerViewLayoutCompleteListener listener) {    this.listener = listener;  }  @Override  publicvoidremoveRecyclerViewLayoutCompleteListener(RecyclerViewLayoutCompleteListener listener) {    if(this.listener != null&& this.listener == listener)    {      this.listener = null;    }  }} | 
Важными частями в деятельности являются:
- метод слушателя, onGlobalLayout (), который сигнализирует, что просмотрщик перекачал свой макет для перерисовки
- логический флаг recyclerViewLayoutCompleted , который используется ресурсом холостого хода, чтобы проверить, может ли тест Espresso продолжать работать после перерисовки в обзоре реселлера.
Это ресурс холостого хода, который будет использоваться для проверки представления переработчика в действии.
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | publicclassRecyclerViewLayoutCompleteIdlingResource implementsIdlingResource {  privateResourceCallback resourceCallback;  privateRecyclerViewIdlingCallback recyclerViewIdlingCallback;  privateRecyclerViewLayoutCompleteListener listener;  publicRecyclerViewLayoutCompleteIdlingResource(RecyclerViewIdlingCallback recyclerViewIdlingCallback){    this.recyclerViewIdlingCallback = recyclerViewIdlingCallback;    listener = newRecyclerViewLayoutCompleteListener() {      @Override      publicvoidonLayoutCompleted() {        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  publicString getName() {    return"RecyclerViewLayoutCompleteIdlingResource";  }  @Override  publicbooleanisIdleNow() {    returnrecyclerViewIdlingCallback.isRecyclerViewLayoutCompleted();  }  @Override  publicvoidregisterIdleTransitionCallback(ResourceCallback resourceCallback) {    this.resourceCallback = resourceCallback;  }} | 
Действие передается ресурсу холостого хода в его конструкторе как реализация RecyclerViewIdlingCallback . Затем, когда просмотр в операторе будет готов, операция вызовет обратный вызов в ресурсе холостого хода, чтобы указать, что он «простаивает».
Наконец, мы можем собрать это вместе в тесте эспрессо.
| 01 02 03 04 05 06 07 08 09 10 11 12 | @TestpublicvoidtestFilterRecyclerViewUsingSearchView(){  // CODE HERE use espresso to use the SearchView to filter the recyclerview  RecyclerViewLayoutCompleteIdlingResource idlingResource = newRecyclerViewLayoutCompleteIdlingResource((RecyclerViewCallbackContactsActivity) activityTestRule.getActivity());  IdlingRegistry.getInstance().register(idlingResource);  // CODE HERE to verify the recyclerview with the updated data  IdlingRegistry.getInstance().unregister(idlingResource);} | 
  Предостережение 
  Я действительно начал писать этот пост некоторое время назад, поэтому код был для Recyclerview из библиотеки поддержки Android, а не из библиотеки Androidx . 
| Опубликовано на Java Code Geeks с разрешения Дэвида Вонга, партнера нашей программы JCG . См. Оригинальную статью здесь: Espresso Idling Resource для изменений данных RecyclerView Мнения, высказанные участниками Java Code Geeks, являются их собственными. |