5

My question is actually quite generic. I want to know how to unit test a Room Dao query that returns a PagingSource From Paging 3.

I have a Room Dao query:

    @Query("SELECT * FROM database")
    fun getChocolateListData(): PagingSource<Int, Chocolate>

I'm wondering how this query can be unit tested.

What I've tried so far (using in-memory Room database for testing):

@FlowPreview
@Test
fun saveChocolateToDbSavesData() = runBlocking {
    val dao: Dao by inject()

    val chocolate = Chocolate(
        name = "Dove"
    )
    dao.saveChocolate(chocolate) 

    val pagingSourceFactory = { dao.getChocolateListData() }
    val pagingDataFlow: Flow<PagingData<Chocolate>> = Pager(
        config = PagingConfig(
            pageSize = 50,
            maxSize = 200,
            enablePlaceholders = false
        ),
        pagingSourceFactory = pagingSourceFactory
    ).flow

    val chocolateListFlow = pagingDataFlow.testIn(coroutinesTestRule)
    Assert.assertEquals(PagingData.from(listOf(chocolate)), chocolateListFlow.emissions[0])
}

This doesn't pass, however:

junit.framework.AssertionFailedError: Expected :androidx.paging.PagingData@7d6c23a1 Actual :androidx.paging.PagingData@321123d2

Not sure how to get it right. Any help would be greatly appreciated!

rabyunghwa
  • 178
  • 3
  • 24

3 Answers3

6

PagingData is wrapper around an internal event stream, you cannot compare it directly and the error you are getting is throwing referential inequality as expected.

Instead you should either query the PagingSource directly to compare the data in LoadResult.Page or you'll need to hook it up to a presenter API such as AsyncPagingDataDiffer or PagingDataAdapter and use .snapshot()

val flow = Pager(..).flow
val adapter = MyPagingDataAdapter()
val job = launch {
    flow.collectLatest { adapter.submitData(it) }
}
// Do your asserts here
job.cancel()

if you need a test scope, I recommend runBlockingTest from the kotlinx.coroutines.test library

To query PagingSource directly, it has a single suspending .load() method, so you can simply wrap it in runBlockingTest and assert the result:

@Test
fun test() = runBlockingTest {
  val pagingSource = MyPagingSource()
  val actual = pagingSource.load(LoadParams.Refresh(...))
  assertEquals(actual as? LoadResult.Page)?.data, listOf(...))
}
dlam
  • 3,547
  • 17
  • 20
  • Thanks for the answer! As I'm only testing Dao queries, I don't want to deal with the UI. Can you tell me how to query the PagingSource directly? Some sample code would help. – rabyunghwa Jan 12 '21 at 10:49
  • I added a small snippet, but essentially you can just called `.load()` directly. Depending on how `.load()` works, you may need to put some effort into setting which dispatchers the actual work is done on, so that you can take advantage of kotlinx.coroutines.test's pause dispatcher / delaycontroller. – dlam Jan 12 '21 at 18:56
  • Again, thanks for the reply. _actual.data_ cannot be resolved. What is the API used here? Do you have the link to the developer doc? I'm currently using _paging-alpha09_. Was it introduced in a later version? – rabyunghwa Jan 13 '21 at 00:30
  • LoadResult.Page is a subclass of LoadResult, so you'll need to check then cast using `as?` I'll update the code sample – dlam Jan 13 '21 at 09:21
  • Could you help me with [this question](https://stackoverflow.com/questions/65810968/how-to-test-pagingdata-from-paging-3)? Thanks in advance! – rabyunghwa Jan 20 '21 at 13:59
  • I have the same code as you for testing but when I run the test several time it has different results on each run, one time successful one time fails. I have posted a question here too https://stackoverflow.com/q/67957325/2280316. would you please take look at it too. thank you. – Bita Mirshafiee Jun 13 '21 at 20:47
2

Based on the answer marked as correct I did my own, is not pretty but at least get the job done if any feedback I would be glad, thanks in advance.

fun <PaginationKey: Any, Model: Any>PagingSource<PaginationKey, Model>.getData(): List<Model> {
    val data = mutableListOf<Model>()
    val latch = CountDownLatch(1)
    val job = CoroutineScope(Dispatchers.Main).launch {
        val loadResult: PagingSource.LoadResult<PaginationKey, Model> = this@getData.load(
            PagingSource.LoadParams.Refresh(
                key = null, loadSize = Int.MAX_VALUE, placeholdersEnabled = false
            )
        )
        when (loadResult) {
            is PagingSource.LoadResult.Error -> throw loadResult.throwable
            is PagingSource.LoadResult.Page -> data.addAll(loadResult.data)
        }
        latch.countDown()
    }
    latch.await()
    job.cancel()
    return data
}

So in your testing, you can use it like this

val obtainedData = myDao.getSomePagingSource().getData()

assertEquals(expectedData, obtainedData)

WARNING: You are gonna see a rather extended log

WARNING: pageSize on the LegacyPagingSource is not set.
When using legacy DataSource / DataSourceFactory with Paging3, page size...
cutiko
  • 9,887
  • 3
  • 45
  • 59
0

Just in case you if need to mock PagingSource:

create helper class PagingSourceUtils.kt Example :

class PagingSourceUtils<T : Any>(
    private val data: List<T>
) : PagingSource<Int, T>() {
    override fun getRefreshKey(state: PagingState<Int, T>): Int? {
        return 0
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> {
        return LoadResult.Page(
            data = data,
            prevKey = null,
            nextKey = null
        )
    }
}

YourTest.kt

@Test
    fun `should success get Chocolate `() {

         val chocolates = listOf(Chocolate(
             name = "Dove"
         )) 

        runBlocking {
            val tData = PagingSourceUtils(chocolates)
            `when`(dao.getChocolateListData()).thenReturn(tData)

            val data = ...

            val actual = ..

            assertEquals(actual, data)
        }
    }