What is the correct way to check the data from a PagingData object in Android Unit Tests
Asked Answered
V

4

21

I am using paging library to retrieve data from an api and show them in a list

for this purpose in my repository I have created the method:

fun getArticleList(query: String): Flow<PagingData<ArticleHeaderData>>

in my viewmodel I have created the search method which goes something like this:

override fun search(query: String) {
    val lastResult = articleFlow
    if (query == lastQuery && lastResult != null)
        return
    lastQuery = query
    searchJob?.cancel()
    searchJob = launch {
        val newResult: Flow<PagingData<ArticleList>> = repo.getArticleList(query)
            .map {
                it.insertSeparators { //code to add separators }.cachedIn(this)
        articleFlow = newResult
        newResult.collectLatest {
            articleList.postValue(it)
        }
    }
}

in order to test my viewmodel I am using the test method PagingData.from to create a flow to return from my mocked repository like so:

whenever(repo.getArticleList(query)).thenReturn(flowOf(PagingData.from(articles)))

and then I retrieve the actual paging data from the articleList LiveData like so:

val data = vm.articleList.value!!

this returns a PagingData<ArticleList> object that I would like to verify it contains the data from the service (i.e. the articles returned by whenever)

the only way I have found to do this is by creating the following extension function:

private val dcb = object : DifferCallback {
    override fun onChanged(position: Int, count: Int) {}
    override fun onInserted(position: Int, count: Int) {}
    override fun onRemoved(position: Int, count: Int) {}
}

suspend fun <T : Any> PagingData<T>.collectData(): List<T> {
    val items = mutableListOf<T>()
    val dif = object : PagingDataDiffer<T>(dcb, TestDispatchers.Immediate) {
        override suspend fun presentNewList(previousList: NullPaddedList<T>, newList: NullPaddedList<T>, newCombinedLoadStates: CombinedLoadStates, lastAccessedIndex: Int): Int? {
            for (idx in 0 until newList.size)
                items.add(newList.getFromStorage(idx))
            return null
        }
    }
    dif.collectFrom(this)
    return items
}

which seems to work, but is based on the PagingDataDiffer class which is marked as @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) so it may not work in the future

is there a better way to either get the flow from PagingData (which is marked as internal in the library) or get the actual data from it?

Vaca answered 21/8, 2020 at 12:6 Comment(5)
Don't suppose you ever found a solution?Sasser
nope, still using this code, hasn't broken down so far, so I guess it's stayingVaca
@Vaca that approch does not seem to work anymore, when trying to test with the extension collectData(), an exception is thrown: Missing call to onListPresentable after new list was presented. If you are seeing this exception, it is generally an indication of an issue with Paging. Please file a bug so we can fix it at: issuetracker.google.com/issues/new?component=413106Cinchonidine
@ZivKesten just call onListPresentable() after the for loop.Stallion
Method signature has been updated and shows a compile error in Android StudioBanket
E
10

I've had the same problem with the Paging3 library, and there not a lot discussions about this library online yet, but as I digging through some the docs, I may found a solution. The scenario I'm facing is trying to determine whether a data list is empty or not within PagingData, then I'll manipulate the UI base on that.

Here's what I found in the doc, there are two apis in PagingDataAdapter that have been added in version 3.0.0-alpha04 which is peek(), and snapshot(), peek() gives us a specific list object based on index, whereas snapshot() gives us the whole list.

So here's what I've done:

lifecycleScope.launch {
    //Your PagingData flow submits the data to your RecyclerView adapter
    viewModel.allConversations.collectLatest {
        adapter.submitData(it)
    }
}
lifecycleScope.launch {
    //Your adapter's loadStateFlow here
    adapter.loadStateFlow.
        distinctUntilChangedBy {
            it.refresh
        }.collect {
            //you get all the data here
            val list = adapter.snapshot()
            ...
        }
    }

Since I just get my hands on the Paging library and Flow recently, there might be flaws with this approach, let me know if there are better ways!

Electrocardiograph answered 29/8, 2020 at 11:22 Comment(2)
your approach seems to work for the actual code, and I have something similar set up (only I use my viewmodel to hold livedata of pageddata and use the flow internally in it instead of exposing the flow itself) but what I'm trying to do is test the data, in unit tests, so in your example how would you test the contents received from viewmodel.allconversations.collectlatest?Vaca
Why distinctUntilChangedBy { it.refresh }? Looks like its skipping appended items.Fifth
C
2

You can do it using AsyncPagingDataDiffer

@Test
fun paging_data_should_not_be_empty()= runTest{
    
    val pagingData = viewModel.pager.first()

    val differ = AsyncPagingDataDiffer(
        diffCallback = TestDiffCallback<RecentChatUi>(),
        updateCallback = TestListCallback(),
        workerDispatcher = Dispatchers.Main
    )

    // You don't need to use launch() if you're using
    // PagingData.from()
    val job = launch {
        differ.submitData(pagingData)
    }

    advanceUntilIdle()
    val pagingList = differ.snapshot().items //this is the data you wanted
    assertTrue(pagingList.isEmpty().not())
    job.cancel()

}

create necessary callbacks to pass on AsyncPagingDataDiffer instance

class TestListCallback : ListUpdateCallback {
    override fun onChanged(position: Int, count: Int, payload: Any?) {}
    override fun onMoved(fromPosition: Int, toPosition: Int) {}
    override fun onInserted(position: Int, count: Int) {}
    override fun onRemoved(position: Int, count: Int) {}
}

class TestDiffCallback<T> : DiffUtil.ItemCallback<T>() {
    override fun areItemsTheSame(oldItem: T, newItem: T): Boolean {
        return oldItem == newItem
    }

    override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
        return oldItem == newItem
    }
}
Cesura answered 9/2, 2023 at 16:38 Comment(1)
It is useful but it is awkward to use it for unit test + compose ui. We dont have any other options tho?Uganda
M
0

I agree with Zijian Wang the only way to test it, is through snapshot enter image description here

Mercorr answered 20/10, 2020 at 3:28 Comment(1)
as I said to Zijian , yes this works in actual code, but I'm running unit tests, there is no lifecyclescope , there is no adapter , and I'm not running robolectric in them to create such objects, they are supposed to be plain unit tests to test basic functionalityVaca
M
-9

This only works when there is an active internet connection.

Myeshamyhre answered 7/1, 2021 at 14:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.