Robolectric Tips: Testing RecyclerViews

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
)
}
}

--

--

--

Documenting my learnings on my journey as a software engineer.

Love podcasts or audiobooks? Learn on the go with our new app.

Android Drawable with Custom States

Android Mid-level Interview questions

GloballyDynamic: Multi-platform dynamic delivery with a unified client API

Kronos-Android: Easy NTP

Navigation with Dynamic Feature Modules

Android Studio 4.0 — Motion Editor

New Android App Review System Could Drive Devs Nuts

Flutter — Persisting data locally using sembast.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Brian Terczynski

Brian Terczynski

Documenting my learnings on my journey as a software engineer.

More from Medium

Changing The Function of The Navigation Button in ActionBar With Jetpack Navigation

You could do this to improve your UI tests performance

Why not to use Volley in 2022?

Peer in OkHttp(1)