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

后端 未结 3 497
佛祖请我去吃肉
佛祖请我去吃肉 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: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  List.asPagedList() = LivePagedListBuilder(createMockDataSourceFactory(this),
            Config(enablePlaceholders = false,
                    prefetchDistance = 24,
                    pageSize = if (size == 0) 1 else size))
            .build().getOrAwaitValue()
    
        private fun  createMockDataSourceFactory(itemList: List): DataSource.Factory =
            object : DataSource.Factory() {
                override fun create(): DataSource = MockLimitDataSource(itemList)
            }
    
        private val mockQuery = mockk {
            every { sql } returns ""
        }
    
        private val mockDb = mockk {
            every { invalidationTracker } returns mockk(relaxUnitFun = true)
        }
    
        class MockLimitDataSource(private val itemList: List) : LimitOffsetDataSource(mockDb, mockQuery, false, null) {
            override fun convertRows(cursor: Cursor?): MutableList = itemList.toMutableList()
            override fun countItems(): Int = itemList.count()
            override fun isInvalid(): Boolean = false
            override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback) { /* Not implemented */ }
    
            override fun loadRange(startPosition: Int, loadCount: Int) =
                itemList.subList(startPosition, startPosition + loadCount).toMutableList()
    
            override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) {
                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  LiveData.getOrAwaitValue(
            time: Long = 2,
            timeUnit: TimeUnit = TimeUnit.SECONDS,
            afterObserve: () -> Unit = {}
        ): T {
            var data: T? = null
            val latch = CountDownLatch(1)
            val observer = object : Observer {
                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)
            }
        }
    

提交回复
热议问题