Android TDD Part 11 — Use custom view to improve testability

Evan Chen
4 min readAug 13, 2020

--

You can create a custom view by combining views. For example, this is a number selector, which put a “-” button, “+” button, and Textview, you can pick a number by clicking “+” or “-”.

The benefit of a custom view is that placing logic in the component. So you only have to test this component, not Activity.

Create custom layout

Create a NumberSelect layout file. This layout includes a “-” Button, “+” Button, and TextView.

<LinearLayout xmlns:android="<http://schemas.android.com/apk/res/android>"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/number_select_background">
<Button
android:id="@+id/minusButton"
android:layout_width="50dp"
android:layout_height="wrap_content"
android:padding="0dp"
android:text="-"
android:textSize="32sp" />
<TextView
android:id="@+id/valueTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:width="30dp"
android:gravity="center"
android:layout_gravity="center_horizontal|center_vertical"
android:textColor="@color/colorPrimary" />
<Button
android:id="@+id/addButton"
android:layout_width="50dp"
android:layout_height="wrap_content"
android:padding="0dp"
android:text="+"
android:textSize="32sp" />
</LinearLayout>

Define custom attributes

To define custom attributes, add a declare-styleable resource to your project. you can set default value, minimum value, and maximum value.

attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="NumberSelect">
<attr name="default_value" format="integer" />
<attr name="min_value" format="integer" />
<attr name="max_value" format="integer" />
</declare-styleable>
</resources>

Setting attribute on layout

Now you can set defaultValue, minValue and maxValue in custom layout.

res/layout/activity_main.xml

<LinearLayout
xmlns:android="<http://schemas.android.com/apk/res/android>"
xmlns:tools="<http://schemas.android.com/tools>"
xmlns:app="<http://schemas.android.com/apk/res-auto>"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_gravity="center"
tools:context=".MainActivity">
<evan.chen.tutorial.tdd.customcomponentsample.NumberSelect
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/number_select"
app:default_value="3"
app:min_value="0"
app:max_value="20"
/>
</LinearLayout>

Create NumberSelect Class

This class loads layout and initial property by attributes. Provide functions to set max value and min value. We also create a NumberSelectListener interface and onValueChange function. You can implement this interface to know value changed.

class NumberSelect : LinearLayout {
private lateinit var addButton: Button
private lateinit var minusButton: Button
private lateinit var valueTextView: TextView
//minimum value
private var minValue: Int = 0
//maximum value
private var maxValue: Int = 0
//default value
private var defaultValue: Int = 0
//now value
var textValue: Int = 0
private var listener: NumberSelectListener? = null
interface NumberSelectListener {
fun onValueChange(value: Int)
}
constructor(context: Context) : super(context) {
init(context, null)
}
constructor(context: Context, attrs: AttributeSet)
: super(context, attrs) {
init(context, attrs)
}
constructor(context: Context,attrs: AttributeSet, defStyle: Int)
: super(context, attrs, defStyle) {
init(context, attrs)
}
private fun init(context: Context, attrs: AttributeSet?) {
View.inflate(context, R.layout.number_select, this)
descendantFocusability = ViewGroup.FOCUS_BLOCK_DESCENDANTS
this.addButton = findViewById(R.id.addButton)
this.minusButton = findViewById(R.id.minusButton)
this.valueTextView = findViewById(R.id.valueTextView)
this.textValue = 0
this.maxValue = Integer.MAX_VALUE
this.minValue = 0
if (attrs != null) {
val attributes = context.theme.obtainStyledAttributes(
attrs,
R.styleable.NumberSelect,
0, 0
)
//get value from layout attributes
this.maxValue = attributes.getInt(
R.styleable.NumberSelect_max_value, this.maxValue
)
this.minValue = attributes.getInt(
R.styleable.NumberSelect_min_value, this.minValue
)
this.defaultValue = attributes.getInt(
R.styleable.NumberSelect_default_value, 0
)
this.valueTextView.text = defaultValue.toString()
this.textValue = defaultValue
}

// Click "+" Button,
// set TextValue plus one and call listener.onValueChange

this.addButton.setOnClickListener {
addTextValue()
if (listener != null) {
listener!!.onValueChange(textValue)
}
}

// Click "-" Button,
// set TextValue minus one and call listener.onValueChange
this.minusButton.setOnClickListener {
minusTextValue()
if (listener != null) {
listener!!.onValueChange(textValue)
}
}
}
fun setMaxValue(value: Int) {
this.maxValue = value
}
fun setMinValue(value: Int) {
this.minValue = value
}
fun setDefaultValue(value: Int) {
this.defaultValue = value
this.textValue = value
}
private fun addTextValue() {
if (this.textValue < this.maxValue) {
this.textValue++
this.valueTextView.text = this.textValue.toString()
}
}
private fun minusTextValue() {
if (this.textValue > this.minValue) {
this.textValue--
this.valueTextView.text = this.textValue.toString()
}
}
fun setListener(listener: NumberSelectListener) {
this.listener = listener
}
}

Use custom component

Now you can use NumberSelect in activity_main.xml.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="<http://schemas.android.com/apk/res/android>"
xmlns:tools="<http://schemas.android.com/tools>"
xmlns:app="<http://schemas.android.com/apk/res-auto>"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_gravity="center"
tools:context=".MainActivity">
<evan.chen.tutorial.tdd.customcomponentsample.NumberSelect
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/number_select"
app:default_value="3"
app:min_value="0"
app:max_value="20"
/>
</LinearLayout>

Writing tests

First, create NumberSelectAndroidTest in AndroidTest. Add a testAddButtonThenValueShouldAdd test function.

When “+” button is clicked, textValue should add one.

@Test
fun testAddButtonThenValueShouldAdd() {
val context = InstrumentationRegistry.getTargetContext()
val numberSelect = NumberSelect(context)
numberSelect.setDefaultValue(1)
numberSelect.addButton.performClick()
Assert.assertEquals(2, numberSelect.textValue)
}

When “-” button is clicked, textValue should minus one.

@Test
fun testMinusButtonThenValueShouldMinus() {
val context = InstrumentationRegistry.getTargetContext()
val numberSelect = NumberSelect(context)
numberSelect.setDefaultValue(2)
numberSelect.minusButton.performClick()
Assert.assertEquals(1, numberSelect.textValue)
}

To verify that textValue can’t less than minValue.

@Test
fun testMinValueLimit() {
val context = InstrumentationRegistry.getTargetContext()
val numberSelect = NumberSelect(context)
numberSelect.setDefaultValue(2)
numberSelect.setMinValue(2)
numberSelect.minusButton.performClick()
Assert.assertEquals(2, numberSelect.textValue)
}

To verify that textValue can’t bigger than maxValue.

@Test
fun testMaxValueLimit() {
val context = InstrumentationRegistry.getTargetContext()
val numberSelect = NumberSelect(context)
numberSelect.setDefaultValue(2)
numberSelect.setMaxValue(2)
numberSelect.addButton.performClick()
Assert.assertEquals(2, numberSelect.textValue)
}

Github:
https://github.com/evanchen76/AndroidTDD_CustomComponent

Reference:
https://developer.android.com/guide/topics/ui/custom-components

Next: Android TDD Part 12 — Set test environment variables from Gradle

--

--