How do I create a PagedList of an object for tests?

后端 未结 3 489
佛祖请我去吃肉
佛祖请我去吃肉 2021-02-07 05:26

I have been working with the arch libraries from Google, but one thing that has made testing difficult is working with PagedList.

For this example, I am usi

相关标签:
3条回答
  • 2021-02-07 05:47

    an easy way to achieve this, is to mock the PagedList. This fun will "convert" a list to a PagedList (in this case, we are not using the real PagedList rather just a mocked version, if you need other methods of PagedList to be implemented, add them in this fun)

     fun <T> mockPagedList(list: List<T>): PagedList<T> {
         val pagedList = Mockito.mock(PagedList::class.java) as PagedList<T>
         Mockito.`when`(pagedList.get(ArgumentMatchers.anyInt())).then { invocation ->
            val index = invocation.arguments.first() as Int
            list[index]
         }
         Mockito.`when`(pagedList.size).thenReturn(list.size)
         return pagedList
     }
    
    0 讨论(0)
  • 2021-02-07 05:51

    Paging 3

    The Paging 3 library offers a builder method PagingData.from(someList).

    Paging 2

    Convert List Into PagedList With a Mock DataSource.Factory.

    @saied89 shared this solution in this googlesamples/android-architecture-components issue. I've implemented the mocked PagedList in the Coinverse Open App in order to local unit test a ViewModel using Kotlin, JUnit 5, MockK, and AssertJ libraries.

    To observe the LiveData from the PagedList I've used Jose Alcérreca's implementation of getOrAwaitValue from the LiveDataSample sample app under Google's Android Architecture Components samples.

    The asPagedList extension function is implemented in the sample test ContentViewModelTest.kt below.

    PagedListTestUtil.kt

    
        import android.database.Cursor
        import androidx.paging.DataSource
        import androidx.paging.LivePagedListBuilder
        import androidx.paging.PagedList
        import androidx.room.RoomDatabase
        import androidx.room.RoomSQLiteQuery
        import androidx.room.paging.LimitOffsetDataSource
        import io.mockk.every
        import io.mockk.mockk
    
        fun <T> List<T>.asPagedList() = LivePagedListBuilder<Int, T>(createMockDataSourceFactory(this),
            Config(enablePlaceholders = false,
                    prefetchDistance = 24,
                    pageSize = if (size == 0) 1 else size))
            .build().getOrAwaitValue()
    
        private fun <T> createMockDataSourceFactory(itemList: List<T>): DataSource.Factory<Int, T> =
            object : DataSource.Factory<Int, T>() {
                override fun create(): DataSource<Int, T> = MockLimitDataSource(itemList)
            }
    
        private val mockQuery = mockk<RoomSQLiteQuery> {
            every { sql } returns ""
        }
    
        private val mockDb = mockk<RoomDatabase> {
            every { invalidationTracker } returns mockk(relaxUnitFun = true)
        }
    
        class MockLimitDataSource<T>(private val itemList: List<T>) : LimitOffsetDataSource<T>(mockDb, mockQuery, false, null) {
            override fun convertRows(cursor: Cursor?): MutableList<T> = itemList.toMutableList()
            override fun countItems(): Int = itemList.count()
            override fun isInvalid(): Boolean = false
            override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<T>) { /* Not implemented */ }
    
            override fun loadRange(startPosition: Int, loadCount: Int) =
                itemList.subList(startPosition, startPosition + loadCount).toMutableList()
    
            override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<T>) {
                callback.onResult(itemList, 0)
            }
        }
    

    LiveDataTestUtil.kt

    
        import androidx.lifecycle.LiveData
        import androidx.lifecycle.Observer
        import java.util.concurrent.CountDownLatch
        import java.util.concurrent.TimeUnit
        import java.util.concurrent.TimeoutException
    
        /**
         * Gets the value of a [LiveData] or waits for it to have one, with a timeout.
         *
         * Use this extension from host-side (JVM) tests. It's recommended to use it alongside
         * `InstantTaskExecutorRule` or a similar mechanism to execute tasks synchronously.
         */
        fun <T> LiveData<T>.getOrAwaitValue(
            time: Long = 2,
            timeUnit: TimeUnit = TimeUnit.SECONDS,
            afterObserve: () -> Unit = {}
        ): T {
            var data: T? = null
            val latch = CountDownLatch(1)
            val observer = object : Observer<T> {
                override fun onChanged(o: T?) {
                    data = o
                    latch.countDown()
                    this@getOrAwaitValue.removeObserver(this)
                }
            }
            this.observeForever(observer)
            afterObserve.invoke()
            // Don't wait indefinitely if the LiveData is not set.
            if (!latch.await(time, timeUnit)) {
                this.removeObserver(observer)
                throw TimeoutException("LiveData value was never set.")
            }
            @Suppress("UNCHECKED_CAST")
            return data as T
        }
    

    ContentViewModelTest.kt

        ...
        import androidx.paging.PagedList
        import com.google.firebase.Timestamp
        import io.mockk.*
        import org.assertj.core.api.Assertions.assertThat
        import org.junit.jupiter.api.AfterAll
        import org.junit.jupiter.api.BeforeAll
        import org.junit.jupiter.api.BeforeEach
        import org.junit.jupiter.api.Test
        import org.junit.jupiter.api.extension.ExtendWith
    
        @ExtendWith(InstantExecutorExtension::class)
        class ContentViewModelTest {
            val timestamp = getTimeframe(DAY)
    
            @BeforeAll
            fun beforeAll() {
                mockkObject(ContentRepository)
            }
    
            @BeforeEach
            fun beforeEach() {
                clearAllMocks()
            }
    
            @AfterAll
            fun afterAll() {
                unmockkAll()
            }
    
            @Test
            fun `Feed Load`() {
                val content = Content("85", 0.0, Enums.ContentType.NONE, Timestamp.now(), "",
                    "", "", "", "", "", "", MAIN,
                    0, 0.0, 0.0, 0.0, 0.0,
                    0.0, 0.0, 0.0, 0.0)
                every {
                    getMainFeedList(any(), any())
                } returns liveData { 
                   emit(Lce.Content(
                       ContentResult.PagedListResult(
                            pagedList = liveData {emit(listOf(content).asPagedList())}, 
                            errorMessage = ""))
                }
                val contentViewModel = ContentViewModel(ContentRepository)
                contentViewModel.processEvent(ContentViewEvent.FeedLoad(MAIN, DAY, timestamp, false))
                assertThat(contentViewModel.feedViewState.getOrAwaitValue().contentList.getOrAwaitValue()[0])
                    .isEqualTo(content)
                assertThat(contentViewModel.feedViewState.getOrAwaitValue().toolbar).isEqualTo(
                    ToolbarState(
                            visibility = GONE,
                            titleRes = app_name,
                            isSupportActionBarEnabled = false))
                verify {
                    getMainFeedList(any(), any())
                }
                confirmVerified(ContentRepository)
            }
        }
    

    InstantExecutorExtension.kt

    This is required for JUnit 5 when using LiveData in order to ensure the Observer is not on the main thread. Below is Jeroen Mols' implementation.

        import androidx.arch.core.executor.ArchTaskExecutor
        import androidx.arch.core.executor.TaskExecutor
        import org.junit.jupiter.api.extension.AfterEachCallback
        import org.junit.jupiter.api.extension.BeforeEachCallback
        import org.junit.jupiter.api.extension.ExtensionContext
    
        class InstantExecutorExtension : BeforeEachCallback, AfterEachCallback {
            override fun beforeEach(context: ExtensionContext?) {
                ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() {
                    override fun executeOnDiskIO(runnable: Runnable) = runnable.run()
                    override fun postToMainThread(runnable: Runnable) = runnable.run()
                    override fun isMainThread(): Boolean = true
                })
            }
    
            override fun afterEach(context: ExtensionContext?) {
                ArchTaskExecutor.getInstance().setDelegate(null)
            }
        }
    
    0 讨论(0)
  • 2021-02-07 05:51
    1. You cannot cast List to PagedList.
    2. You cannot create PagedList directly, only through DataSource. One way is creating FakeDataSource returning the test data.

    If it is an end-to-end test, you could just use in-memory db. Add your test data before calling it. Example: https://medium.com/exploring-android/android-architecture-components-testing-your-room-dao-classes-e06e1c9a1535

    0 讨论(0)
提交回复
热议问题