ABOUT ME

-

  • Android) Jetpack Paging3 유닛 테스트 해보자
    Android 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

    반응형

    댓글

Designed by Me.