Robolectric Tips: Testing RecyclerViews

Brian Terczynski
3 min readOct 10, 2021

--

How to get ViewHolders to render in RecyclerViews when testing in Robolectric

Photo by Denny Müller on Unsplash

Like most other Views in Android, RecyclerViews can also be tested in Robolectric. For example, you can verify the contents of their ViewHolders and test interactions with them such as clicks. However if you simply try to launch the Activity or Fragment that contains the RecyclerView and set up its data, the RecyclerView itself will not have any ViewHolders rendered. You actually have to explicitly measure and layout the RecyclerView in order to get it to inflate and render its ViewHolders when run from Robolectric.

Let’s take an example. Suppose you have an Activity with a Fragment that has a RecyclerView in it. And you write a RobolectricTest for that Activity/Fragment to verify the contents of the RecyclerView’s items. To make things simple, we’ll write a test to verify that a single item has the correct text displayed:

@RunWith(RobolectricTestRunner::class)
class MainActivityTest {
@Test
fun test() {
// Test fixture
val textOfFirstItem = "This is a test"
// Launch our Activity
val activityController = Robolectric.buildActivity(
MainActivity::class.java
)
.setup()
val activity = activityController.get()
// Get the fragment (assuming we just have one)
val fragment = activity
.supportFragmentManager
.fragments[0] as MainFragment
// Get the recyclerView from the fragment
val myRecyclerView = fragment.myRecyclerView
val adapter = myRecyclerView.adapter as MyAdapter
// Set the data in the RecyclerView
// Assume here that setValues() calls notifyDataSetChanged()
adapter.setValues(
listOf(
MyEntity(
title = textOfFirstItem
)
)
)
// Pull out the first ViewHolder
val viewHolder = myRecyclerView
.findViewHolderForAdapterPosition(0)
// Verify it's text is correct
assertEquals(
textOfFirstItem,
(viewHolder as MyViewHolder)
.itemView
.findViewById<TextView>(R.id.name)
.text
)
}
}

This code will actually crash in the assertEquals() because the value of viewHolder is null . The RecyclerView has no inflated ViewHolders yet.

And the reason is because the RecyclerView was not laid out. It was not given dimensions nor laid out, and as such it thinks it has no visible area to display, and as such has not inflated any of its ViewHolders. And that’s because it’s a Robolectric test and we’re not actually running this on an emulator. Everything is “faked” or “shadowed”, as it were.

But there’s a little trick we can do. We can simply tell the RecyclerView it’s measurements and tell it to lay itself out. Once we do that, it should inflate all of the ViewHolders that fit within the dimensions we specify. And we can do that with these two simple lines:

myView.measure(
View.MeasureSpec.UNSPECIFIED,
View.MeasureSpec.UNSPECIFIED
)
myView.layout(0, 0, 1000, 1000)

By passing UNSPECIFIED to the .measure() function, you are saying that there are no parent-imposed restrictions on the size that this view wants to be. (You could get fancier and create specific MeasureSpecs if you want, but UNSPECIFIED should suffice for most test cases.) Then you pass in the dimensions this view will have to the layout() method. Keep in mind that if you want to verify the contents of several ViewHolders, you’ll need to ensure the RecyclerView is big enough that it will actually display all of those items.

So now you can just insert those two lines into the above test, and it should work!

@RunWith(RobolectricTestRunner::class)
class MainActivityTest {
@Test
fun test() {
// Test fixture
val textOfFirstItem = "This is a test"
// Launch our Activity
val activityController = Robolectric.buildActivity(
MainActivity::class.java
)
.setup()
val activity = activityController.get()
// Get the fragment (assuming we just have one)
val fragment = activity
.supportFragmentManager
.fragments[0] as MainFragment
// Get the recyclerView from the fragment
val myRecyclerView = fragment.myRecyclerView
val adapter = myRecyclerView.adapter as MyAdapter
// Set the data in the RecyclerView
// Assume here that setValues() calls notifyDataSetChanged()
adapter.setValues(
listOf(
MyEntity(
title = textOfFirstItem
)
)
)
// Lay out the RecyclerView with particular dimensions,
// so that it will actually inflate its ViewHolders
myRecyclerView.measure(
View.MeasureSpec.UNSPECIFIED,
View.MeasureSpec.UNSPECIFIED
)
myRecyclerView.layout(0, 0, 1000, 1000)
// Pull out the first ViewHolder
val viewHolder = myRecyclerView
.findViewHolderForAdapterPosition(0)
// Verify it's text is correct
assertEquals(
textOfFirstItem,
(viewHolder as MyViewHolder)
.itemView
.findViewById<TextView>(R.id.name)
.text
)
}
}

--

--

Brian Terczynski
Brian Terczynski

Written by Brian Terczynski

Documenting my learnings on my journey as a software engineer.

Responses (1)