Automating Android NumberPicker using Espresso

While writing an Android UI test involving a NumberPicker, I discovered that instrumenting the NumberPicker using Espresso was not as straightforward as some other Android controls.

Background

The basic pattern for testing a UI element in Espresso is to use a view matcher to find a view, and then perform some action on it. For example, simply clicking a button might look something like this:


onView(withId(R.id.magic_button)).perform(click());

However, there are no convenient methods for scrolling a NumberPicker. The scrollTo() action sounded promising, but unfortunately it only works on a descendant of ScrollView. And NumberPicker is just a plain old ViewGroup.

Setting the Value

My next thought was to use a custom view action and just call setValue() on the NumberPicker directly:


onView(withId(pickerId)).perform(new ViewAction() {
    @Override
    public Matcher getConstraints() {
        return ViewMatchers.isAssignableFrom(NumberPicker.class);
    }

    @Override
    public String getDescription() {
        return "Set the value of a NumberPicker";
    }

    @Override
    public void perform(UiController uiController, View view) {
        ((NumberPicker)view).setValue(value);
    }
});

Although this did indeed update the NumberPicker with the desired value, it did not fire any event listeners–which is actually how it should be! A value set by code, rather than by the user, should not fire event listeners. Though Android is inconsistent regarding this behavior, at least it is correct here. But I digress…

If I had a way to get at the NumberPicker’s event listeners, I could possibly fire the events myself. But this could potentially cause different behavior due to the way that the NumberPicker fires events, or due to future changes to its implementation.

Ideally, I would like to simulate a swipe on the NumberPicker and let it deal with dispatching events in the same way that it would in production.

Simulating Touch Events

It just so happens that Espresso also provides the ability to simulate clicks and swipes on views. In my case, I needed a way to swipe up or down depending on the value my test was trying to set. What I came up with is a loop that continually swipes the NumberPicker to find the desired value:


public static void selectNumberPickerValue(int pickerId, int targetValue, ActivityTestRule activityTestRule) {
    final int ROWS_PER_SWIPE = 5;
    NumberPicker numberPicker = (NumberPicker)activityTestRule.getActivity().findViewById(pickerId);
    ViewInteraction viewInteraction = onView(withId(pickerId));

    while (targetValue != numberPicker.getValue()) {
        int delta = Math.abs(targetValue - numberPicker.getValue());
        if (targetValue < numberPicker.getValue()) {
            if (delta >= ROWS_PER_SWIPE) {
                viewInteraction.perform(new GeneralSwipeAction(Swipe.FAST, GeneralLocation.TOP_CENTER, GeneralLocation.BOTTOM_CENTER, Press.FINGER));
            } else {
                viewInteraction.perform(new GeneralClickAction(Tap.SINGLE, GeneralLocation.TOP_CENTER, Press.FINGER));
            }
        } else {
            if (delta >= ROWS_PER_SWIPE) {
                viewInteraction.perform(new GeneralSwipeAction(Swipe.FAST, GeneralLocation.BOTTOM_CENTER, GeneralLocation.TOP_CENTER, Press.FINGER));
            } else {
                viewInteraction.perform(new GeneralClickAction(Tap.SINGLE, GeneralLocation.BOTTOM_CENTER, Press.FINGER));
            }
        }
        SystemClock.sleep(50);
    }
}

I found that swiping does not scroll a huge number of items (usually about five), but it is still faster than tapping through items one at a time. However, it’s also possible to overshoot, so we just revert to single clicks when we’re getting close. A short sleep at the end prevents sending more events than can be processed (the UI takes some time to animate).

Conclusion

While this solution gets the job done and accurately simulates real user interaction, it can potentially slow down a test if the NumberPicker needs to be scrolled a long way. I’m just happy that it didn’t require any event listener hacking or other code duplication.

Conversation
  • Jani says:

    Hi! This post has been a life saver. I’ve been struggling with this for some time, trying various approaches to no avail, effectively halting my project since I’m too obsessive-compulsive to move on. Your solution works perfectly. I did have to replace viewInteraction with onView(withId(pickerId)) since the variable wasn’t introduced in the example.

    I don’t think that using a NumberPicker with thousands of values would be good design anyway, so I’d say that this works adequately for all conceivable cases. Thanks again!

    • Brian Vanderwal Brian Vanderwal says:

      Hi Jani, I’m glad it worked for you! NumberPicker can be frustrating to work with in a number of ways. And thanks for the update – I added the missing line to the code.

  • Comments are closed.