Unit Test for Paging3
Tags: kotlin, jetpack, android
Published: 2023-02-14 04:16:14

### What is Paging3
Paging3 is a jetpack library that allows us to easily load large datasets from the data source (local, remote, file, etc. ). It loads data gradually, reducing network and system resources usage. It is written in Kotlin and works in coordination with other Jetpack libraries.

### Dependency
First, We have to add this dependency to our **build.gradle**

```
implementation "androidx.paging:paging-runtime-ktx:3.1.1"
implementation "androidx.paging:paging-common-ktx:3.1.1"
testImplementation "io.mockk:mockk:1.12.5"
testImplementation "junit:junit:4.13.2"
testImplementation "io.mockk:mockk:1.12.5"
```



### Paging Source File
So we have **SpecialicationPagingSource** like this:

```
class SpecializationPagingSource(
    private val professionUid: String,
    private val query: SpecializationQuery,
    private val api: CommonService
) : PagingSource<Int, SpecializationDomain>() {

    companion object {
        private const val STARTING_PAGE = 1
    }

    override fun getRefreshKey(state: PagingState<Int, SpecializationDomain>): Int? {
        return null
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, SpecializationDomain> {
        val currentPage = params.key.takeIf { it != 0 } ?: STARTING_PAGE

        return try {
            val result = withContext(Dispatchers.IO) {
                ApiHandler.handleApi {
                    api.getSpecializationList(professionUid = professionUid, query = query.toMap())
                }
            }

            val totalPages = result?.meta?.pagination?.totalPage ?: 0
            val data = result?.data?.record ?: listOf()

            LoadResult.Page(
                data = data.map { it.toDomain() },
                prevKey = null,
                nextKey = if (currentPage < totalPages) currentPage + 1 else null
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }
}
```
let's write out test case :)


### Positive Case
In positive case, we can set **PagingSource** return value with **LoadResult.page**. for example:

`LoadResult.Page(data = null, prevKey = null, nextKey = null)`

We have 2 test scenarios for our **PagingSource** . **Refresh** and **Append** . **Refresh** is when our **PagingSource** load first page, and **Append** is when our **PagingSource** load next pages.

for **Refresh** test, we can write our test like this:

        //given
        val fakeResponse = GeneralResponseWrapper(
            data = GeneralRecordHolder(record = listOf(specializationResponse)),
            meta = GeneralMetaResponse(
                pagination = pagination = GeneralPaginationResponse(
                    page = 1,
                    totalPage = 1,
                    limit = 1,
                    totalRecords = 1,
                    records = 1
                )
            )
        )
        val expectedResult =
            PagingSource.LoadResult.Page(
                data = listOf(specializationResponse).map { it.toDomain() },
                prevKey = null,
                nextKey = null
            )

        //when
        coEvery {
            mockService.getSpecializationList(any(), any())
        } returns Response.success(fakeResponse)

        //then
        Assert.assertEquals(
            expectedResult,
            specializationPagingSource.load(
                PagingSource.LoadParams.Refresh(
                    key = 1,
                    loadSize = 1,
                    placeholdersEnabled = false
                )
            )
        )

        coVerify {
            mockService.getSpecializationList(any(), any())
        }

please look at **GeneralPaginationResponse** class. We set **totalPage = 1** because we want to test only when paging load data for first time. Then we use **PagingSource.LoadParams.Refresh** for refresh.


for **Append** test, we can write our test like this:

        //given
        val fakeResponse = GeneralResponseWrapper(
            data = GeneralRecordHolder(record = listOf(specializationResponse)),
            meta = GeneralMetaResponse(
                pagination = GeneralPaginationResponse(
                    page = 1,
                    totalPage = 2,
                    limit = 1,
                    totalRecords = 1,
                    records = 1
                )
            )
        )

        val expectedResult =
            PagingSource.LoadResult.Page(
                data = listOf(specializationResponse).map { it.toDomain() },
                prevKey = null,
                nextKey = 2
            )

        //when
        coEvery {
            mockService.getSpecializationList(any(), any())
        } returns Response.success(fakeResponse)

        //then
        Assert.assertEquals(
            expectedResult,
            specializationPagingSource.load(
                PagingSource.LoadParams.Append(
                    key = 1,
                    loadSize = 1,
                    placeholdersEnabled = false
                )
            )
        )

        coVerify {
            mockService.getSpecializationList(any(), any())
        }

please look at **GeneralPaginationResponse** class. We set **totalPage = 2** because we want to paging have 2 page to load with **limit = 1** per page . please take a look out **expectedResult** variable. the next **key = 2** means we expect out paging load page 2. Then we use **PagingSource.LoadParams.Append** for append next page to existing loaded data.




### Negative Case
In negative case, we can set **PagingSource** return value with **LoadResult.Error**. for example:

`PagingSource.LoadResult.Error<Int, T>(Exception("error data"))`

And our codes will look like this:

       //given
        val expectedResult =
            PagingSource.LoadResult.Error<Int, SpecializationDomain>(BadRequestException("error data"))

        //when
        coEvery {
            mockService.getSpecializationList(any(), any())
        } throws expectedResult.throwable

        //then
        Assert.assertEquals(
            expectedResult,
            specializationPagingSource.load(
                PagingSource.LoadParams.Refresh(
                    key = 1,
                    loadSize = 1,
                    placeholdersEnabled = false
                )
            )
        )

        coVerify {
            mockService.getSpecializationList(any(), any())
        }




## Sample Code:

```
class SpecializationPagingSource(
    private val professionUid: String,
    private val query: SpecializationQuery,
    private val api: CommonService
) : PagingSource<Int, SpecializationDomain>() {

    companion object {
        private const val STARTING_PAGE = 1
    }

    override fun getRefreshKey(state: PagingState<Int, SpecializationDomain>): Int? {
        return null
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, SpecializationDomain> {
        val currentPage = params.key.takeIf { it != 0 } ?: STARTING_PAGE

        return try {
            val result = withContext(Dispatchers.IO) {
                ApiHandler.handleApi {
                    api.getSpecializationList(professionUid = professionUid, query = query.toMap())
                }
            }

            val totalPages = result?.meta?.pagination?.totalPage ?: 0
            val data = result?.data?.record ?: listOf()

            LoadResult.Page(
                data = data.map { it.toDomain() },
                prevKey = null,
                nextKey = if (currentPage < totalPages) currentPage + 1 else null
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }
}
```


```
@OptIn(ExperimentalCoroutinesApi::class)
class SpecializationPagingSourceTest : BaseUnitTestDataLayer() {
    lateinit var specializationPagingSource: SpecializationPagingSource

    @MockK
    lateinit var mockService: CommonService

    @RelaxedMockK
    lateinit var specializationQuery: SpecializationQuery

    companion object {
        val specializationResponse = SpecializationResponse(
            "1",
            "dokter hewan"
        )
    }

    @Before
    override fun setUp() {
        super.setUp()
        specializationPagingSource =
            SpecializationPagingSource("thisIsUid", specializationQuery, mockService)
    }

    @Test
    fun `when SpecializationPagingSource refresh return Success`() = runTest {

        //given
        val fakeResponse = GeneralResponseWrapper(
            data = GeneralRecordHolder(record = listOf(specializationResponse)),
            meta = GeneralMetaResponse(
                pagination = pagination = GeneralPaginationResponse(
                    page = 1,
                    totalPage = 2,
                    limit = 1,
                    totalRecords = 1,
                    records = 1
                )
            )
        )
        val expectedResult =
            PagingSource.LoadResult.Page(
                data = listOf(specializationResponse).map { it.toDomain() },
                prevKey = null,
                nextKey = null
            )

        //when
        coEvery {
            mockService.getSpecializationList(any(), any())
        } returns Response.success(fakeResponse)

        //then
        Assert.assertEquals(
            expectedResult,
            specializationPagingSource.load(
                PagingSource.LoadParams.Refresh(
                    key = 1,
                    loadSize = 1,
                    placeholdersEnabled = false
                )
            )
        )

        coVerify {
            mockService.getSpecializationList(any(), any())
        }

    }

    @Test
    fun `when SpecializationPagingSource append return Success`() = runTest {

        //given
        val fakeResponse = GeneralResponseWrapper(
            data = GeneralRecordHolder(record = listOf(specializationResponse)),
            meta = GeneralMetaResponse(
                pagination = GeneralPaginationResponse(
                    page = 1,
                    totalPage = 2,
                    limit = 1,
                    totalRecords = 1,
                    records = 1
                )
            )
        )

        val expectedResult =
            PagingSource.LoadResult.Page(
                data = listOf(specializationResponse).map { it.toDomain() },
                prevKey = null,
                nextKey = 2
            )

        //when
        coEvery {
            mockService.getSpecializationList(any(), any())
        } returns Response.success(fakeResponse)

        //then
        Assert.assertEquals(
            expectedResult,
            specializationPagingSource.load(
                PagingSource.LoadParams.Append(
                    key = 1,
                    loadSize = 1,
                    placeholdersEnabled = false
                )
            )
        )

        coVerify {
            mockService.getSpecializationList(any(), any())
        }

    }

    @Test
    fun `when SpecializationPagingSource return Exception`() = runTest {

        //given
        val expectedResult =
            PagingSource.LoadResult.Error<Int, SpecializationDomain>(BadRequestException("error data"))

        //when
        coEvery {
            mockService.getSpecializationList(any(), any())
        } throws expectedResult.throwable

        //then
        Assert.assertEquals(
            expectedResult,
            specializationPagingSource.load(
                PagingSource.LoadParams.Refresh(
                    key = 1,
                    loadSize = 1,
                    placeholdersEnabled = false
                )
            )
        )

        coVerify {
            mockService.getSpecializationList(any(), any())
        }

    }

}
```

Reference: [https://medium.com/@mohamed.gamal.elsayed/android-how-to-test-paging-3-pagingsource-433251ade028](https://medium.com/@mohamed.gamal.elsayed/android-how-to-test-paging-3-pagingsource-433251ade028)