Статьи

Ресурс ожидания эспрессо для изменений данных RecyclerView

У меня возникла проблема с использованием 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)
public class RecyclerViewIdlingResourceTest {
 
@Rule
public 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 и прохождения теста.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
@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 (), но любой Android-эквивалент с обработчиками и т. Д. Тоже бы сработал. Конечно, все это немного взломать. Рекомендуемый способ подождать завершения какого-либо процесса, прежде чем Espresso продолжит тестирование, — использовать Idling Resources .

Использование паузы в течение некоторого произвольного периода времени не является идеальным, так как это часто приводит либо к нестабильным тестам, либо к тому, что тесты выполняются дольше, чем необходимо.

Обратный вызов RecyclerView

Для работы ресурса бездействия необходимо знать, когда RecyclerView RecyclerView перерисовывается с новыми данными списка.

Существуют различные обратные вызовы, которые есть у RecyclerView (и его классов поддержки), которые могут сигнализировать о том, что RecyclerView находится в процессе перерисовки. После поиска в StackOverflow я нашел следующие возможности:

Я решил использовать onGlobalLayoutListener , но кажется, что RecyclerView может сигнализировать о перерисовке несколькими способами.

The Idling Resource слушает в …

Во-первых, нам нужны некоторые интерфейсы для использования в качестве обратных вызовов для связи между RecyclerView, действием / фрагментом, содержащим RecyclerView и Idling Resource.

Во-первых, это интерфейс для RecyclerView, который уведомляет об активности, когда происходит процесс перерисовки с новыми данными.

01
02
03
04
05
06
07
08
09
10
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();
}

Затем другой интерфейс для использования в качестве обратного вызова для действия, чтобы уведомить Ресурс бездействия к … простоя.

1
2
3
4
5
public interface RecyclerViewLayoutCompleteListener {
 
// Callback to notify the idling resource that it can transition to the idle state
public void onLayoutCompleted();
}

Вот пример действия, показывающий только соответствующий код для работы с ресурсом холостого хода.

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
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 (), который сигнализирует, что просмотрщик перекачал свой макет для перерисовки
  • логический флаг 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
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 . Затем, когда просмотр в операторе будет готов, операция вызовет обратный вызов в ресурсе холостого хода, чтобы указать, что он «простаивает».

Наконец, мы можем собрать это вместе в тесте эспрессо.

01
02
03
04
05
06
07
08
09
10
11
12
@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);
}

Предостережение
Я действительно начал писать этот пост некоторое время назад, поэтому код был для Recyclerview из библиотеки поддержки Android, а не из библиотеки Androidx .

Опубликовано на Java Code Geeks с разрешения Дэвида Вонга, партнера нашей программы JCG . См. Оригинальную статью здесь: Espresso Idling Resource для изменений данных RecyclerView

Мнения, высказанные участниками Java Code Geeks, являются их собственными.