NumberPicker

NumberPicker: Espresso Testing

In the previous article we looked at some of the problems that we can have when working with the largely unloved NumberPicker widget. Another area which can be a little tricky with NumberPicker is when it comes to writing Espresso tests for it. In this article we’ll look at some techniques for creating Espresso tests for NumberPicker.

The main reason that NumberPicker is tricky to test is that it is really a compound control. The actual components will change depending on whether we’re using the Material themed variant or one of the older variants. I’m assuming that most people reading this will be using the Material themed variant, so the discussion will only be for that.

NumberPicker has a child view which is an EditText that permits manual editing of the numeric value. To obtain the current value of the NumberPicker we can actually check the text value of this control.

Let’s begin by looking at a simple Espresso test which verifies that the NumberPicker is visible:

@RunWith(AndroidJUnit4::class)
class NumberPickerTest {

    @get:Rule
    val activityRule = ActivityTestRule(MainActivity::class.java)

    @Test
    fun testNumberPicker() {
        onNumberPicker()
            .check(matches(isDisplayed()))

    }

    private fun onNumberPicker() = onView(withId(R.id.number_picker))
}

I have created a utility method named onNumberPicker() which provides a consistent View matcher which we’ll use repeatedly in our tests. I’m assuming that most people reading this are familiar with the fundamentals of Espresso, but for those that aren’t I would suggest familiarising yourself before we go any further.

In the sample app for the previous article we created a NumberPicker which displays the range of numbers from 0 to 10, and has wrapSelectorWheel = true set which causes the numbers to wrap once we reach the end of the range, so we get a continuous, infinite loop of the numbers from 0 to 10. We don’t set a default value so we would expect this to begin at 0. We should test this initial state, so we need a way of verifying the current value. As previously mentioned, there is a child EditText which holds a text value, and we can easily match on this without knowing anything about it other than the fact that its parent is our NumberPicker:

@RunWith(AndroidJUnit4::class)
class NumberPickerTest {

    @get:Rule
    val activityRule = ActivityTestRule(MainActivity::class.java)

    @Test
    fun testNumberPicker() {
        onNumberPicker()
            .check(matches(isDisplayed()))
        onNumberPickerInput()
            .check(matches(withText("0")))
    }

    private fun onNumberPicker() = onView(withId(R.id.number_picker))
    private fun onNumberPickerInput() = onView(withParent(withId(R.id.number_picker)))
}

The enabler here is the onNumberPickerInput() function which is almost identical to onNumberPicker() except that it used the withParent matcher to find a view which has the NumberPicker as a parent which will be the EditText. We can then verify that this contains the expected text.

So we have a way of verifying the current value selected within the NumberPicker, but we also want to be able to check other aspects of the behaviour such as when the user taps the increment and decrement areas of the control. Normally with Espresso we use a click() view action to perform a click on a specific control. This is a little trickier with NumberPicker because it has distinct tap zones which perform distinct actions. Tapping above the top divider will decrement the value; Tapping between the dividers will edit the value of the child EditText; And tapping below the bottom divider will increment the value.

Although NumberPicker behaves as though there are separate controls for each of these three areas but there are not actual views for the decrement and increment areas, different behaviours are performed depending upon where in the NumberPicker the touch event occurred. The click() view action will perform a click in the centre of the control, so this doesn’t allow us to perform taps in the decrement and increment areas.

If we look at the source of click() view action we can see that it uses a GeneralClickAction to perform a click in the middle of the control. We can copy this, and use a different GeneralLocation constant to perform the click at a different point of within the NumberPicker view bounds. For decrement we do this at the top centre point, and for increment it is at the bottom centre:

@RunWith(AndroidJUnit4::class)
class NumberPickerTest {
    .
    .
    .
    private val clickTopCentre =
        actionWithAssertions(
            GeneralClickAction(
                Tap.SINGLE,
                GeneralLocation.TOP_CENTER,
                Press.FINGER,
                InputDevice.SOURCE_UNKNOWN,
                MotionEvent.BUTTON_PRIMARY
            )
        )

    private val clickBottomCentre =
        actionWithAssertions(
            GeneralClickAction(
                Tap.SINGLE,
                GeneralLocation.BOTTOM_CENTER,
                Press.FINGER,
                InputDevice.SOURCE_UNKNOWN,
                MotionEvent.BUTTON_PRIMARY
            )
        )
}

I’ve included these within my test class for simplicity, but if I were using these within a more complex test suite I would be inclined to create common actions rather than embed them within a test class as I have here.

With those in place we can add some further checks around the decrement and increment behaviours:

@RunWith(AndroidJUnit4::class)
class NumberPickerTest {

    @get:Rule
    val activityRule = ActivityTestRule(MainActivity::class.java)

    @Test
    fun testNumberPicker() {
        onNumberPicker()
            .check(matches(isDisplayed()))
        onNumberPickerInput()
            .check(matches(withText("0")))

        onNumberPicker()
            .perform(clickBottomCentre)
        onNumberPickerInput()
            .check(matches(withText("1")))

        onNumberPicker()
            .perform(clickTopCentre)
        onNumberPickerInput()
            .check(matches(withText("0")))

        onNumberPicker()
            .perform(clickTopCentre)
        onNumberPickerInput()
            .check(matches(withText("10")))
    }
    .
    .
    .
}

The first two of these test the basic increment and decrement behaviours, and the third one tests the wrap around behaviour – if the current value is 0 and decrement is tapped, then the value wraps to 10. If wrapping was disabled, then we’d need to change this test to verify that the value remained at 0 after clicking decrement.

Although NumberPicker also supports swipe and long press interactions, the intention here is not to test the control itself, but the behaviour that we have configured it with – the range of values supported, and the wrapping behaviour. For that purpose these tests are more than adequate. If the number range or wrapping were to change without updating the tests to match then they would fail.

Although NumberPicker at first appears difficult to create Espresso tests for, an understanding of it’s anatomy and internal behaviours enables us to quite easily create some utilities which make testing pretty straightforward.

The source code for this article is available here.

© 2020, Mark Allison. All rights reserved.

Copyright © 2020 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

1 Comment

Leave a Reply to fuengfa Cancel reply

Your email address will not be published. Required fields are marked *

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