Testing Delayed Tasks in Robolectric

Brian Terczynski
2 min readDec 12, 2021

--

Let’s say you need to test an update to your user interface after a time delay. For example, a filter bar. You enter text in the filter bar, and it will filter out items below that match that particular term. The filter happens after you change the text, so you don’t have to hit any kind of button to make it happen. However, the filtering is delayed by 1 second, so that your app isn’t filtering after every keystroke (to make things more efficient and give the user some time to finish typing in their filter term). Here is an example from a tasks app that I wrote:

So let’s say we want to write a Robolectric test to ensure that, after you set text in the filter bar, it shows the appropriate elements below. We can definitely set the text in the filter bar from Robolectric with something like this:

fragment.binding.filterBar.setText("laund")

And let’s say, for simplicity, we’re just checking the item count (goes from 2 to 1 after setting the filter text). If our filter bar did an immediate filtering, then our Robolectric test could just be this:

assertEquals(2, adapter.itemCount)
fragment.binding.filterBar.setText("laund")
assertEquals(1, adapter.itemCount)

However, our filter bar has a delay. Any time the text changes, it will cancel out any pending tasks that were there, and replace with a new task to update the items in the RecyclerView after 1 second. Thus, if the filter bar changes in < 1 second, it will cancel that prior task and reissue a new one:

binding.filterBar.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}

override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}

override fun afterTextChanged(s: Editable?) {
view?.handler?.apply {
removeCallbacks(runnable)
postDelayed(runnable, 1000)
}
}

})

So what we need to do in our Robolectric test is force that delayed task to run. And we can do that with a call to ShadowLooper.runUiThreadTasksIncludingDelayedTasks() like so:

assertEquals(2, adapter.itemCount)
fragment.binding.filterBar.setText("laund")
ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
assertEquals(1, adapter.itemCount)

And so now we can test that the state of our UI is correct even after a delayed task.

The ShadowLooper class has a variety of methods that make testing with the Looper a lot easier from Robolectric.

--

--

Brian Terczynski
Brian Terczynski

Written by Brian Terczynski

Documenting my learnings on my journey as a software engineer.

No responses yet