У меня возникла проблема с использованием 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 {@Rulepublic ActivityRule<MainActivity> activityRule = new ActivityRule<MainActivity>(MainActivity.class);// number of items in the original listint allItemsCount = ...;// number of items after the list has been filteredint filteredItemsCount = ...;@Testpublic 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
|
@Testpublic 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 я нашел следующие возможности:
- 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
|
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 idlepublic boolean isRecyclerViewLayoutCompleted();} |
Затем другой интерфейс для использования в качестве обратного вызова для действия, чтобы уведомить Ресурс бездействия к … простоя.
|
1
2
3
4
5
|
public interface RecyclerViewLayoutCompleteListener {// Callback to notify the idling resource that it can transition to the idle statepublic 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
|
@Testpublic 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, являются их собственными. |