DEV Community

Samuel
Samuel

Posted on

Unit Test for Paging3

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"
Enter fullscreen mode Exit fullscreen mode

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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())
    }
Enter fullscreen mode Exit fullscreen mode

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())
    }
Enter fullscreen mode Exit fullscreen mode

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())
    }
Enter fullscreen mode Exit fullscreen mode

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
@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())
        }

    }

}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)