Android

Android) Jetpack Paging3 유닛 테스트 해보자

가짜 개발자 2021. 11. 28. 18:47


Paging 3

  • Android Jetpack에서 제공하는 페이징을 위한 라이브러리입니다.
  • 성능, 메모리, 비용 측면에서 효율적입니다.

 

PagingSource

  • 네트워크 또는 데이터 베이스에서 데이터를 로드하는 추상 클래스. key 타입을 정의하여 구현합니다.
  • LoadResult라는 sealed class에서 응답 처리와 에러 핸들링에 도움을 주기 때문에, 자체적으로 결과 클래스를 만들어 래핑 할 필요가 없습니다.
  • 로드할 다음 페이지가 없으면 nullnextKey에 전달, 이전 페이지가 없으면 prevKey에 전달합니다.
class PicsumPagingSource(private val service: PicsumService) : PagingSource<Int, Item>() {
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Item> {
        return try {
            val page = params.key ?: 0
            val items = service.getImages(page, 10)
            LoadResult.Page(
                data = items,
                prevKey = if (page == 0) null else page - 1,
                nextKey = if (items.isEmpty()) null else page + 1
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }
 }
  • Paging Source를 테스트를 하기 전에 필요한 코드들을 선언해 줬습니다.
  • InstantTaskExecutorRule은 작업을 동일한 스레드에서 동기적으로 처리하게 해줍니다.
  • 코루틴을 사용하기 때문에 TestCoroutineDispathcer를 사용합니다.
@ExperimentalCoroutinesApi
class PicsumPagingSourceTest {

    @get:Rule
    val mainCoroutineRule = MainCoroutineRule()

    @get:Rule
    val instanceExecutorRule = InstantTaskExecutorRule()

    private val service: PicsumService = mockk()

    private lateinit var picsumPagingSource: PicsumPagingSource

    @Before
    fun setUp() {
        picsumPagingSource = PicsumPagingSource(service)
    }
  • 이벤트를 발생시키는 key는 LoadPrarms입니다. LoadPrarms는 seald class이며 3 가지의 타입이 있습니다.
  • Refersh - 초기 로드 또는 새로고침.
  • Append - 데이터 페이지를 로드하고 목록의 끝에 추가.
  • Prepend - 데이터 페이지를 로드하고 목록의 시작 부분에 추가.

 

Paging Source - failure / error 테스트

@Test
fun `paging source load failure received io exception`() = mainCoroutineRule.runBlockingTest {
     val error = IOException("404", Throwable())

     coEvery { service.getImages(any(), any()) } throws error

     val expectedResult = PagingSource.LoadResult.Error<Int, Item>(error)

     Assert.assertEquals(
         expectedResult, picsumPagingSource.load(
             PagingSource.LoadParams.Refresh(
                 key = 0,
                 loadSize = 1,
                 placeholdersEnabled = false
             )
         )
     )
 }

@Test
fun `paging source load failure received null exception`() = mainCoroutineRule.runBlockingTest {
     coEvery { service.getImages(any(), any()) } throws NullPointerException()

     val expectedResult = PagingSource.LoadResult.Error<Int, Item>(NullPointerException())

     Assert.assertEquals(
         expectedResult.toString(), picsumPagingSource.load(
             PagingSource.LoadParams.Refresh(
                 key = 0,
                 loadSize = 1,
                 placeholdersEnabled = false
             )
         ).toString()
     )
 }

 

Paigng Sourcerefresh 테스트

@Test
fun `paging source refresh received success`() = mainCoroutineRule.runBlockingTest {
     coEvery { service.getImages(any(), any()) } returns mockItemList

     service.getImages(0, 10)

     coVerify { service.getImages(any(), any()) }

     val expectedResult =
         PagingSource.LoadResult.Page(data = mockItemList, prevKey = null, nextKey = 1)

     Assert.assertEquals(
         expectedResult, picsumPagingSource.load(
             PagingSource.LoadParams.Refresh(
                 key = 0,
                 loadSize = 10,
                 placeholdersEnabled = false
             )
         )
     )
 }

 

Paigng Source - append 테스트

@Test
fun `paging source append received success`() = mainCoroutineRule.runBlockingTest {
     coEvery { service.getImages(any(), any()) } returns mockItemList

     service.getImages(0, 10)

     coVerify { service.getImages(any(), any()) }

     val expectedResult =
         PagingSource.LoadResult.Page(data = mockItemList, prevKey = 0, nextKey = 2)

     Assert.assertEquals(
         expectedResult, picsumPagingSource.load(
                PagingSource.LoadParams.Append(
                    key = 1,
                    loadSize = 10,
                    placeholdersEnabled = false
                )
         )
     )
 }

 

Paging Source - prepend 테스트

@Test
fun `paging source prepend received success`() = mainCoroutineRule.runBlockingTest {
     coEvery { service.getImages(any(), any()) } returns mockItemList

     service.getImages(0, 10)

     coVerify { service.getImages(any(), any()) }

     val expectedResult =
         PagingSource.LoadResult.Page(data = mockItemList, prevKey = null, nextKey = 1)

     Assert.assertEquals(
         expectedResult, picsumPagingSource.load(
             PagingSource.LoadParams.Prepend(
                 key = 0,
                 loadSize = 10,
                 placeholdersEnabled = false
             )
         )
     )
 }

 

RemoteMediator

  • 네트워크 및 로컬 데이터베이스에서 페이징 데이터를 로드하는 역할.
  • 로컬 데이터베이스를 데이터 소스로 활용함으로써, 네트워크 연결이 불안정할 시 좋은 방법.
  • load() 메서드가 정확한 MediatorResult를 반환하는지 테스트해봅니다.
@ExperimentalPagingApi
class PicsumRemoteMediator(
    private val service: PicsumService,
    private val database: PicsumDatabase
) : RemoteMediator<Int, Item>() {
	...
}
  • 테스트에 필요한 초기 작업을 해줍니다. 데이터베이스를 테스트하기 편하게 인메모리 방식으로, 실제 데이터베이스에 저장되지 않고, 인메모리에 저장되어 프로세스 종료 시에 사라지는 메모리 형태로 구현해 줍니다.
  • Room 데이터 베이스르 만들기 위해서는 Context가 필요하기 때문에, AndroidJUnit4를 빌드하여 애플리케이션 콘텍스트에 접근할 수 있게 해야 합니다.
@ExperimentalCoroutinesApi
@ExperimentalPagingApi
@RunWith(AndroidJUnit4::class)
class PicsumRemoteMediatorTest {

    private lateinit var db: PicsumDatabase
    private val service: PicsumService = mockk()

    private lateinit var picsumRemoteMediator: PicsumRemoteMediator

    @Before
    fun setUp() {
        val context = ApplicationProvider.getApplicationContext<Context>()
        db = Room.inMemoryDatabaseBuilder(context, PicsumDatabase::class.java).build()

        picsumRemoteMediator = PicsumRemoteMediator(
            service,
            db
        )
    }
    
    @After
    fun closeDb() {
        db.close()
    }
  • 해당 테스트는 성공적인 응답을 반환하지만, 반환된 데이터가 비어있는 경우입니다.
  • load() 메서드는 MediaResult.Success를 반환하고, endOfPaginationReached 속성은 true입니다.
@Test
fun refreshLoadReturnsSuccessResult() = runBlocking {
     coEvery { service.getImages(0, 10) } returns listOf()

     val pagingState = PagingState<Int, Item>(
         listOf(),
         null,
         PagingConfig(10),
         10
     )

     val result = picsumRemoteMediator.load(LoadType.REFRESH, pagingState)

     Assert.assertTrue(result is RemoteMediator.MediatorResult.Success)
     Assert.assertTrue((result as RemoteMediator.MediatorResult.Success).endOfPaginationReached)
}

Preference

Paging 구현 테스트

How to test Paging 3

반응형