Testing Data Binding

Chuck Greb
Android Testing
Published in
4 min readSep 11, 2017
ropes” by Chris Blakeley licensed under CC BY-NC-ND 2.0

Data binding on Android requires a slightly different approach to unit testing versus traditional MVP.

Unit testing without data binding

Let’s revisit our simple finance calculator demo app. Here is the original configuration of the Controller/Activity/Presenter before adding data binding.

MainController.kt

interface MainController {
fun setResultText(text: String)
fun setResultColor(color: Color)
}

MainActivity.kt

class MainActivity : AppCompatActivity(), MainController {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
val presenter = MainPresenter(this)
add_button.setOnClickListener {
presenter.onAddButtonClick(
input_text_1.text.toString(),
input_text_2.text.toString())
}
}
override fun setResultText(text: String) {
result_text.text = text
}
override fun setResultColor(color: Color) {
val colorInt = if (color == Color.GREEN)
getColor(R.color.green) else getColor(R.color.red)
result_text.setTextColor(colorInt)
}
}

MainPresenter.kt

class MainPresenter(private val controller: MainController) {
fun onAddButtonClick(input_1: String, input_2: String) {
val calculator = Calculator()
val result = calculator.add(
input_1.toInt(),
input_2.toInt())
controller.setResultText(result.toString())
val category = calculator.getResultCategory(result)
val color = if (category == Category.HIGH)
Color.GREEN else Color.RED
controller
.setResultColor(color)
}
}

With this app configuration, unit testing the presenter involves implementing a fake controller to replace the activity in the test harness. We then test the interaction between the presenter and the fake controller.

MainPresenterTest.kt

class MainPresenterTest {
private val controller = TestMainController()
private val presenter = MainPresenter(controller)
@Test
fun onAddButtonClick_shouldSetResultTextPositive() {
presenter.onAddButtonClick("3", "4")
assertThat(controller.text).isEqualTo("7")
}
@Test
fun onAddButtonClick_shouldSetResultTextNegative() {
presenter.onAddButtonClick("2", "-3")
assertThat(controller.text).isEqualTo("-1")
}
@Test
fun onAddButtonClick_shouldSetResultColorPositive() {
presenter.onAddButtonClick("3", "4")
assertThat(controller.color).isEqualTo(Color.GREEN)
}
@Test
fun onAddButtonClick_shouldSetResultColorNegative() {
presenter.onAddButtonClick("2", "-3")
assertThat(controller.color).isEqualTo(Color.RED)
}
}

TestMainController.kt

class TestMainController : MainController {
var text: String? = null
var color
: Color? = null
override fun setResultText(text: String) {
this.text = text
}
override fun setResultColor(color: Color) {
this.color = color
}
}

Unit testing with data binding

With data binding there is less direct interaction between the presenter and the controller. The domain model is now bound directly to the view.

Now instead of testing the interaction with the controller we can test using the state of the domain model.

The add button has been removed. Using data binding events a new sum is calculated whenever there is a change in either input text field.

When the value of the Result domain model changes this triggers an immediate update in the UI.

The role of the presenter has somewhat changed, as it is now the listener for the text change events and no longer sets the result text on the controller.

It does still however contain the custom presentation logic to color code HIGH and LOW values.

MainController.kt

interface MainController {
fun bindData(result: Result)
fun setResultColor(color: Color)
}

MainActivity.kt

class MainActivity : AppCompatActivity(), MainController {
private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil
.setContentView(this, R.layout.activity_main)

setSupportActionBar(toolbar)

val presenter = MainPresenter(this)
binding.presenter = presenter
}

override fun bindData(result: Result) {
binding.result = result
}

override fun setResultColor(color: Color) {
val colorInt = if (color == Color.GREEN)
getColor(R.color.green) else getColor(R.color.red)
result_text.setTextColor(colorInt)
}
}

MainPresenter.kt

init {
controller.bindData(result)
}

fun onInput1TextChanged(s: CharSequence, start: Int,
before: Int, count: Int) {
calculator.addend1 = if (isNumber(s))
s.toString().toInt() else 0
controller.setResultColor(if (result.category == Category.HIGH)
Color.GREEN else Color.RED)
}

fun onInput2TextChanged(s: CharSequence, start: Int,
before: Int, count: Int) {
calculator.addend2 = if (isNumber(s))
s.toString().toInt() else 0
controller.setResultColor(if (result.category == Category.HIGH)
Color.GREEN else Color.RED)
}

Unit testing the new presenter with data binding relies less on the interaction with the fake controller and more on observing the state of the domain model.

TestMainController.kt

class TestMainController : MainController {
var result: Result? = null
var color
: Color? = null

override fun
bindData(result: Result) {
this.result = result
}

override fun setResultColor(color: Color) {
this.color = color
}
}

MainPresenterTest.kt

class MainPresenterTest {
private val controller = TestMainController()
private val presenter = MainPresenter(controller)

@Test
fun onTextChanged_shouldSetResultTextPositive() {
presenter.onInput1TextChanged("3", 0, 0, 0)
presenter.onInput2TextChanged("4", 0, 0, 0)
assertThat(controller.result?.value?.get()).isEqualTo(7)
}

@Test
fun onTextChanged_shouldSetResultTextNegative() {
presenter.onInput1TextChanged("2", 0, 0, 0)
presenter.onInput2TextChanged("-3", 0, 0, 0)
assertThat(controller.result?.value?.get()).isEqualTo(-1)
}

@Test
fun onTextChanged_shouldSetResultColorPositive() {
presenter.onInput1TextChanged("3", 0, 0, 0)
presenter.onInput2TextChanged("4", 0, 0, 0)
assertThat(controller.color).isEqualTo(Color.GREEN)
}

@Test
fun onTextChanged_shouldSetResultColorNegative() {
presenter.onInput1TextChanged("2", 0, 0, 0)
presenter.onInput2TextChanged("-3", 0, 0, 0)
assertThat(controller.color).isEqualTo(Color.RED)
}
}

The only pseudo-presentation logic that is no longer tested is that which is contained in the data binding expressions in the layout file.

activity_main.xml

<TextView
android:id="@+id/result_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{Integer.toString(result.value)}"
/>

Converting the raw integer value of the result to a string now lives in the view layer and as such can be integration tested using either Robolectric or Espresso.

The full code for this example is available on Github.

Happy Testing!

This post is part of a series on clean architecture that explores how classic software design principles can be applied to modern Android development.

If you found this article helpful, please give it some applause 👏 to help others find it. Feel free to leave a comment below.

--

--

Chuck Greb
Android Testing

Mission-driven engineering leader. Community organizer. Digital minimalist.