【问题标题】:How do I create a PagedList of an object for tests?如何为测试创建对象的 PagedList?
【发布时间】:2018-05-20 14:22:13
【问题描述】:

我一直在使用 Google 的 Arch 库,但让测试变得困难的一件事是使用 PagedList

在本例中,我使用存储库模式并从 API 或网络返回详细信息。

所以在 ViewModel 中我调用了这个接口方法:

override fun getFoos(): Observable<PagedList<Foo>>

然后,存储库将使用 RxPagedListBuilder 创建 PagedList 类型的 Observable

 override fun getFoos(): Observable<PagedList<Foo>> =
            RxPagedListBuilder(database.fooDao().selectAll(), PAGED_LIST_CONFIG).buildObservable()

我希望能够让测试从这些返回PagedList&lt;Foo&gt; 的方法中设置返回值。类似于

when(repository.getFoos()).thenReturn(Observable.just(TEST_PAGED_LIST_OF_FOOS)

两个问题:

  1. 这可能吗?
  2. 如何创建PagedList&lt;Foo&gt;

我的目标是以更端到端的方式进行验证(例如确保屏幕上显示正确的 Foos 列表)。片段/活动/视图是从 ViewModel 观察 PagedList&lt;Foo&gt; 的那个。

【问题讨论】:

  • 我发现您的问题中缺少的是:您想在测试中进行什么验证?
  • @arekolek 感谢您的反馈...我用我要验证的内容更新了问题
  • 关于您实施@isuPatches 的策略的任何更新?我正在使用 Kotlin、JUnit 5 和 MockK 编写相同的测试。一旦概念验证生效,我很高兴在这里分享我的解决方案。

标签: android kotlin android-testing android-architecture-components android-paging


【解决方案1】:

第 3 页

Paging 3 库提供了一个构建器方法PagingData.from(someList)

分页 2

使用 Mock DataSource.Factory 将列表转换为 PagedList。

@saied89 在这个googlesamples/android-architecture-components 问题中分享了这个solution。我在 Coinverse Open App 中实现了模拟的 PagedList,以便使用 Kotlin、JUnit 5、MockK 和 AssertJ 库对 ViewModel 进行本地单元测试。

为了观察 PagedList 中的 LiveData,我在 Google 的 Android 架构组件示例下使用了 getOrAwaitValue 中的 Jose Alcérreca's getOrAwaitValue

asPagedList扩展函数在下面的示例测试ContentViewModelTest.kt中实现。

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

JUnit 5 在使用 LiveData 时需要这样做,以确保 Observer 不在主线程上。下面是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)
        }
    }

【讨论】:

  • 分页 3 lib 会不会类似?
  • 好问题@Raghunandan!我还没有测试 Paging 3 库。如果您实施并发现更改,请使用 cmets/edits 跟进帖子。
【解决方案2】:

实现此目的的一种简单方法是模拟 PagedList。这个乐趣会将一个列表“转换”为一个 PagedList(在这种情况下,我们没有使用真正的 PagedList 而只是一个模拟版本,如果您需要实现 PagedList 的其他方法,请在这个乐趣中添加它们)

 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
 }

【讨论】:

  • 别忘了先添加 Mockito:testImplementation "org.mockito:mockito-core:2.25.0"
  • 你能把 List 转换成 PagedList @bsobat 吗?
  • @bsobat,这应该返回填充数据的PagedList 还是空的PagedList 以避免测试失败?如果为空,则对于测试应用如何处理PagedList 数据将无用。
  • @Hurwitz 不,你不能投射
  • @bsobat,查看上面的solution,它使用 MockK 来实现在编写本地 JUnit 5 测试时使用 PagedList 的行为。
【解决方案3】:
  1. 您不能将 List 转换为 PagedList。
  2. 不能直接创建PagedList,只能通过DataSource。一种方法是创建返回测试数据的 FakeDataSource。

如果是端到端测试,您可以只使用内存数据库。在调用它之前添加您的测试数据。例子: https://medium.com/exploring-android/android-architecture-components-testing-your-room-dao-classes-e06e1c9a1535

【讨论】:

  • 感谢您的洞察力!我有 implemented 一个使用 MockK 进行本地 JUnit 5 测试的模拟 PagedList。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-09-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-12-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多