Android Core

Espresso Idling Resource for RecyclerView Data Changes

I was having a problem with using Android Espresso to test a RecyclerView when it’s data was updated.

This is for an Android app where a list of contacts is displayed by a RecyclerView. There is a SearchView in the action bar that can filter the contacts list to display matching contact names.

The Espresso test ran like this:

  • Start the activity.
  • Espresso verifies that the full list of contacts is displayed in the RecyclerView. This works fine.
  • A query string is entered in a SearchView, and the filtering of the data in the RecyclerView is initiated (I’m using a SearchView to get the query string, but other controls such as EditText, etc, could be used instead).
  • Espresso verifies that the list of contacts has changed to only display matching items. Fails.
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));
}
}

Unfortunately it seems like that the Espresso assert to verify that the list of items has changed happens before the RecyclerView has finished reloading the updated data and redrawing itself. So the test fails when it finds that the RecyclerView still has the original number of items because it has not yet redrawn itself with the new list of data.

The code for this post is in this gist. It is in the form of incomplete code that only includes stuff relevant to the post. Also there are various ways of implementing and filtering a RecyclerView, so I will leave that part to the reader.

So, What’s the Problem?

After I have changed the data for the RecyclerView, I am calling notifyDataSetChanged() on the adapter.

Based on this StackOverflow question, the issue seems to be that when notifyDataSetChanged() is called, it only invalidates the data in the RecyclerView, but doesn’t update the widget immediately. Hence I suspected that the Espresso assertion was happening before the RecyclerView had updated.

To test this, I introduced a pause before the Espresso assertion to allow time for the RecyclerView to update, and the test passed.

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));
}

Here I was using Thread.sleep() but any Android equivalent with Handlers, etc, would have done too. Of course this is all a bit of a hack. The recommended way to introduce a wait for some process to complete before Espresso continues to test is to use Idling Resources.

Using a pause for some arbitrary time period is less than ideal, as it often leads to either flaky tests or making the tests run longer than necessary.

The RecyclerView Callback

For an Idling Resource to work, it needs to know when the RecyclerView RecyclerView is redrawing with the new list data.

There are various callbacks that the RecyclerView (and its support classes) have that can signal that the RecyclerView is in the process of redrawing. After searching on StackOverflow, I found these possibilities:

I decided to use onGlobalLayoutListener, but there seem to be multiple ways for the RecyclerView to signal that it being redrawn.

The Idling Resource is listening in …

Firstly we need some interfaces to use as callbacks to communicate between the RecyclerView, the activity/fragment containing the RecyclerView and Idling Resource.

Firstly here is an interface for the RecyclerView to notify the activity when the redrawing process with new data has occurred.

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();
}

Then another interface to use as a callback for the activity to notify the Idling Resource to .. idle.

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();
}

Here is an example activity, showing just the relevant code to work with the idling resource.

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;
    }
  }
}

The important parts in the activity are:

  • the listener method, onGlobalLayout(), which signals the recyclerview has inflated it’s layout for the redraw
  • the Boolean flag, recyclerViewLayoutCompleted, which is used by the idling resource to check if the Espresso test can continue to run after the recyclerview redraw.

This is the idling resource to be used to test the recyclerview in the activity.

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;
  }
}

The activity is passed to the idling resource in it’s constructor as an RecyclerViewIdlingCallback implementation. Then when the recyclerview in the activity is ready, the activity will invoke the callback in the idling resource to indicate that it is ‘idle’.

Finally we can put this together in an Espresso test.

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);
}

Caveat
I actually started writing this post a while ago, so the code was for the Recyclerview from the Android support library rather than from the Androidx library.

Published on Java Code Geeks with permission by David Wong, partner at our JCG program. See the original article here: Espresso Idling Resource for RecyclerView Data Changes

Opinions expressed by Java Code Geeks contributors are their own.

David Wong

David is a software developer who has worked in the UK, Japan and Australia. He likes building apps in Java, web and Android technologies.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Elena Gillbert
4 years ago

Hi…
I’m Elena gillbert.An idling resource represents an asynchronous operation whose results affect subsequent operations in a UI test. By registering idling resources with Espresso, you can validate these asynchronous operations more reliably when testing your app.

Back to top button