But why?
I use databinding a lot in my Android projects. It fits nicely with ViewModel and general MVVM approach. I also make use of databinding in small layouts – such as RecyclerView row or reusable <include> layouts.
How does databinding works in general?
You enable special build feature in build.gradle:
android { (...) buildFeatures { dataBinding true }}
You add <layout> root in layout xml:
<?xml version="1.0" encoding="utf-8"?><layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content"> (...) </androidx.constraintlayout.widget.ConstraintLayout></layout>
You add <data> tag inside <layout>
… and bind variable to some layout element with syntax android:text=”@{your_variable_name}”
<?xml version="1.0" encoding="utf-8"?><layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_margin="@dimen/standard_margin" android:text="@{title}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="parent" app:layout_constraintStart_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" tools:text="Lorem ipsum" /> </androidx.constraintlayout.widget.ConstraintLayout> <data> <variable name="title" type="String" /> </data></layout>
You hope that build will succeed and binding class would be generated.
Then you can use generated java class with your layout xml and attach it to activity, RecyclerView elements or some custom views. I don’t remember when I had to type something like findViewById(R.id.button) or use synthetic access.
Why test that?
Simply – because I have feeling that it could make sense. It may also be funny exercise.
„Your scientists were so preoccupied with whether or not they could that they didn’t stop to think if they should.” Jurassic Park
Databinding classes are part of your codebase, even if they are generated by framework. Some pieces of logic may exists within them. For example – binding view visibility in XML based on a boolean flag – with mapping to View.VISIBLE and View.GONE or View.INVISIBLE (there will be proper example further).
Of course that kind of things should be well tested in ViewModel (e.x when text input does not match email regex then disable button).
So we can think of testing databinding generated classes as an intermediate layer of test pyramid – between classic unit test and integration tests.
We could also call it pseudo-integration tests.
But what are we testing?
We have given two elements: layout defined in xml and class with data for that view.
I will try to assert few things:
- avatar (image URL) is bound to ImageView
- text is set to TextView
- red pencil icon is visible when canEdit flag is set to true, otherwise it’s gone.
I created layout xml with basic databinding operations:
First ImageView has property bind:image=”@{viewItem.avatar}” which will call following binding adapter:
package tech.michalik.databindingmonstrosityimport android.widget.ImageViewimport androidx.databinding.BindingAdapterinterface ImageLoader { fun loadImageFromUrl(imageView: ImageView, url: String)}class GlideImageLoader : ImageLoader { override fun loadImageFromUrl(imageView: ImageView, url: String) { TODO("Not yet implemented") }}class ImageBindings(private val imageLoader: ImageLoader) : ImageLoader by imageLoader { @BindingAdapter("bind:image") fun ImageView.bindImage(imageUrl: String) { loadImageFromUrl(this, imageUrl) }}
TextView has text set directly android:text=”@{viewItem.text}” – since there already is a way to set String to TextView there is no need to provide additional adapter.
Last ImageView (edit icon) has actual piece of logic in XML – elvis operator. Based on Boolean flag visibility is set to View.VISIBLE or View.GONE.
android:visibility=”@{viewItem.canEdit ? View.VISIBLE : View.GONE}”
Generated code
After successful build java class ListItemBindingImpl will be generated:
To test this we should consider few things:
- which method should we invoke?
- where assertions should be done?
- how to create instance of that class?
But… how?
Let’s start with the easiest part – adding view displayable instance and passing that to ListItemBindingImpl stub:
package tech.michalik.databindingmonstrosity.databindingimport io.kotlintest.specs.StringSpecimport tech.michalik.databindingmonstrosity.ListItemDisplayableclass ListItemBindingImplTest : StringSpec({ "it should do something"{ val displayable = ListItemDisplayable( text = "This is the content", avatar = "http://some-kind-of-cdn.net/asd.jpg", canEdit = false ) val binding: ListItemBindingImpl = TODO() binding.viewItem = displayable binding.executeBindings() }})
you can use TODO() function to create stubs easily, as it returns kotlin.Nothing. I like to use it to satisfy compiler and to go on with further code.
So in test case we are creating displayable instance, we set that variable to binding and invoke executeBidings().
Now let’s take a look at ListItemBindingImpl constructors:
In public constructor we have mapBindings method, I wonder what it does?
We see another mapBindings invocation, but with more complex implementation:
Really long method. Too much effort needed to mock all that stuff. And I am too lazy to analyze what is actually happening here.
Let’s forget about public constructor and check again what is happening in private one:
Alright, that is now more understandable. Array of objects represents view bindings, where each element of array is instance of TextView, ImageView or ConstraintLayout. Wouldn’t it be more convenient to use that constructor?
Using reflection to hack things.
Our goal at a moment is to somehow pass mocks of ImageViews and TextViews so we can perform assertions on them in our test cases. However, we don’t have direct access to constructor accepting that views (we would have to manually declare view hierarchy). So let’s make private constructor accessible using reflection.
Don’t do that at home. Or do it only at home, not at work. Or do it with extra caution, since reflection is double-edged sword.
package tech.michalik.databindingmonstrosity.databindingimport android.view.Viewimport androidx.databinding.DataBindingComponentimport java.lang.reflect.Constructorfun createBinding( bindingComponent: DataBindingComponent, rootView: View, bindings: Array<View>): ListItemBindingImpl { return ListItemBindingImpl::class.java .let { it.declaredConstructors.last() as Constructor<ListItemBindingImpl> }.also { it.isAccessible = true }.newInstance( bindingComponent, rootView, bindings )}
What’s happening here?
- get declared constructors of that class
- set isAccessible to true
- manually create newInstance
Now we can replace stub in our test with something that looks more like system under test:
package tech.michalik.databindingmonstrosity.databindingimport androidx.databinding.DataBindingComponentimport com.nhaarman.mockitokotlin2.mockimport io.kotlintest.specs.StringSpecimport tech.michalik.databindingmonstrosity.ImageBindingsimport tech.michalik.databindingmonstrosity.ListItemDisplayableclass ListItemBindingImplTest : StringSpec({ "it should do something"{ val displayable = ListItemDisplayable( text = "This is the content", avatar = "http://some-kind-of-cdn.net/asd.jpg", canEdit = false ) val binding: ListItemBindingImpl = createBinding( bindingComponent = object : DataBindingComponent { override fun getImageBindings(): ImageBindings { TODO("Not yet implemented") } }, rootView = mock(), bindings = emptyArray() ) binding.viewItem = displayable binding.executeBindings() }})
After running this test… Hooray! It actually invoked private constructor.
Now we can go further. Let’s create mocks of views that we are going to interact with:
Let’s check which view should be on which position in bindings array and provide mocks with Mockito:
package tech.michalik.databindingmonstrosity.databindingimport android.widget.ImageViewimport android.widget.TextViewimport androidx.constraintlayout.widget.ConstraintLayoutimport androidx.databinding.DataBindingComponentimport com.nhaarman.mockitokotlin2.mockimport io.kotlintest.specs.StringSpecimport tech.michalik.databindingmonstrosity.ImageBindingsimport tech.michalik.databindingmonstrosity.ListItemDisplayableclass ListItemBindingImplTest : StringSpec({ "it should do something"{ val displayable = ListItemDisplayable( text = "This is the content", avatar = "http://some-kind-of-cdn.net/asd.jpg", canEdit = false ) val bindings = arrayOf( mock<ConstraintLayout>(), mock<ImageView>(), mock<TextView>(), mock<ImageView>() ) val binding: ListItemBindingImpl = createBinding( bindingComponent = object : DataBindingComponent { override fun getImageBindings(): ImageBindings { TODO("Not yet implemented") } }, rootView = mock(), bindings = bindings ) binding.viewItem = displayable binding.executeBindings() }})
Other error, that’s magnificent! We can proceed!
Now let’s solve that problem.
Only non-null check? Let’s just ensure it won’t be null here. I will mock static method looper with Mockk – since I already have Mockk on classpath (you can probably use PowerMock here or something like that).
package tech.michalik.databindingmonstrosity.databindingimport android.os.Looperimport android.widget.ImageViewimport android.widget.TextViewimport androidx.constraintlayout.widget.ConstraintLayoutimport androidx.databinding.DataBindingComponentimport com.nhaarman.mockitokotlin2.mockimport io.kotlintest.specs.StringSpecimport io.mockk.everyimport io.mockk.mockkimport io.mockk.mockkStaticimport tech.michalik.databindingmonstrosity.ImageBindingsimport tech.michalik.databindingmonstrosity.ListItemDisplayableclass ListItemBindingImplTest : StringSpec({ mockkStatic(Looper::class) every { Looper.myLooper() } returns mockk<Looper>() "it should do something"{ val displayable = ListItemDisplayable( text = "This is the content", avatar = "http://some-kind-of-cdn.net/asd.jpg", canEdit = false ) val bindings = arrayOf( mock<ConstraintLayout>(), mock<ImageView>(), mock<TextView>(), mock<ImageView>() ) val binding: ListItemBindingImpl = createBinding( bindingComponent = object : DataBindingComponent { override fun getImageBindings(): ImageBindings { TODO("Not yet implemented") } }, rootView = mock(), bindings = bindings ) binding.viewItem = displayable binding.executeBindings() }})
Hit run and…
Great success! Our code actually reached method execute bindings and failed because ImageBindings fake is not implemented!
Actual tests
Now the boring and well-known part. Writing mocks with Mockito and interactions verifications.
Nothing fancy here. Just regular use of mocks.
Summary
I did very weird thing, but it was interesting experiment. With regular tools (Mockito, Reflection, Android Studio) I was able to create actual tests for generated databinding classes.
Is databinding code worth testing? Probably not. For sure it’s worth to know what is happening under the hood. But covering with test something that is generated by framework may be not the best idea. That stuff may change at any moment.
I don’t consider this code proper, valid test that I would like to have in production projects because:
- mocks are provided via private constructor (reflection hacks)
- view model tests should cover those cases (if separation of concerns is well implemented)
- mocking static methods (Handler, Looper) was needed (and it may guide to non-deterministic behavior)
- for larger layouts it would require a lot (I mean really a lot) of unused mocks
To sum this up – I had fun experiment with reflection, but results are not that beneficiary as I hoped.
Check repository with code here
(You can star this repo if you want, I will appreciate it)
Tools I used:
https://developer.android.com/topic/libraries/data-binding
Ready to take your software skills to the next level? Schedule consultation with me.