代码之家  ›  专栏  ›  技术社区  ›  isuPatches

如何创建测试对象的页面列表?

  •  12
  • isuPatches  · 技术社区  · 6 年前

    我一直在使用google的arch库,但是有一件事让测试变得困难 PagedList 是的。

    对于本例,我使用存储库模式并从api或网络返回详细信息。

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

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

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

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

    我希望能够让测试设置从这些返回 PagedList<Foo> 是的。类似于

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

    两个问题:

    1. 这可能吗?
    2. 如何创建一个 页面列表<foo> 是吗?

    我的目标是以更端到端的方式进行验证(比如确保在屏幕上显示正确的foos列表)。片段/活动/视图是观察 页面列表<foo> 从视图模型。

    2 回复  |  直到 6 年前
        1
  •  8
  •   bsobat    6 年前

    要做到这一点,一个简单的方法就是模仿pagelist。这个乐趣将“转换”一个列表到一个页面列表(在这种情况下,我们不是使用真正的页面列表,而只是一个模拟版本,如果你需要实现页面列表的其他方法,在这个乐趣中添加它们)

     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
     }
    
        2
  •  0
  •   Deividas Strioga    6 年前
    1. 不能将列表强制转换为PagedList。
    2. 不能仅通过数据源直接创建页面列表。一种方法是创建返回测试数据的fakedatasource。

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

        3
  •  0
  •   AdamHurwitz    5 年前

    使用mock datasource.factory将列表转换为PagedList

    @saied89 分享了这个 solution 在这个 googlesamples/android-architecture-components 问题。我在 Coinverse Open App 为了使用kotlin、junit 5、mockk和assertj库对viewmodel进行本地单元测试。

    从我使用过的页面列表中观察实时数据 Jose Alcérreca's implementation 属于 getOrAwaitValue LiveDataSample sample app 在google的android架构组件示例下。

    这个 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(config: PagedList.Config? = null): PagedList<T>? {
        val defaultConfig = PagedList.Config.Builder()
            .setEnablePlaceholders(false)
            .setPageSize(size)
            .setMaxSize(size + 2)
            .setPrefetchDistance(1)
            .build()
        return LivePagedListBuilder<Int, T>(
            createMockDataSourceFactory(this),
            config ?: defaultConfig
        ).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 MutableLiveData<Lce<ContentResult.PagedListResult>>().also { lce ->
                lce.value = Lce.Content(
                    ContentResult.PagedListResult(
                        pagedList = MutableLiveData<PagedList<Content>>().apply {
                            this.value = listOf(content).asPagedList(
                                PagedList.Config.Builder().setEnablePlaceholders(false)
                                    .setPrefetchDistance(24)
                                    .setPageSize(12)
                                    .build())
                            }, 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

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