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.
Thank you so much. <3