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
Top comments (0)