web-dev-qa-db-fra.com

Comment créer une PagedList d'un objet pour les tests?

J'ai travaillé avec les bibliothèques Arch de Google, mais une chose qui a rendu les tests difficiles est de travailler avec PagedList.

Pour cet exemple, j'utilise le modèle de référentiel et je renvoie des détails à partir d'une API ou d'un réseau.

Donc, dans le ViewModel, j'appelle cette méthode d'interface:

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

Le référentiel utilisera alors RxPagedListBuilder pour créer le Observable qui est de type PagedList:

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

Je veux pouvoir effectuer des tests pour configurer le retour de ces méthodes qui renvoient un PagedList<Foo>. Quelque chose de similaire à

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

Deux questions:

  1. Est-ce possible?
  2. Comment créer un PagedList<Foo>?

Mon objectif est de vérifier de manière plus complète (en veillant par exemple à ce que la liste correcte des Foos s'affiche à l'écran). Le fragment/activité/vue est celui qui observe le PagedList<Foo> à partir d'un ViewModel.

17
isuPatches

un moyen simple d'y parvenir est de se moquer de la PagedList. Ce plaisir "convertira" une liste en PagedList (dans ce cas, nous n'utilisons pas le vrai PagedList plutôt qu'une version simulée, si vous avez besoin d'autres méthodes de PagedList pour être implémentées, ajoutez-les dans ce plaisir)

 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
 }
9
bsobat

Convertir la liste en PagedList avec Mock DataSource.Factory

@ saied89 a partagé ceci solution dans ce googlesamples/Android-architecture-components problème. J'ai implémenté la PagedList simulée dans Coinverse Open App afin de tester un module local un ViewModel en utilisant les bibliothèques Kotlin, JUnit 5, MockK et AssertJ.

Pour observer les LiveData de la PagedList, j'ai utilisé de Jose Alcérrecaimplémentation de getOrAwaitValue de exemple d'application LiveDataSample sous Google's = Android Exemples de composants d'architecture.

La fonction d'extension asPagedList est implémentée dans l'exemple de test ContentViewModelTest.kt ci-dessous.

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()
                [email protected](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

Ceci est requis pour JUnit 5 lors de l'utilisation de LiveData afin de s'assurer que l'observateur n'est pas sur le thread principal. Ci-dessous est Jeroen Mols 'implémentation .

    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)
        }
    }
2
Adam Hurwitz
  1. Vous ne pouvez pas convertir List en PagedList.
  2. Vous ne pouvez pas créer PagedList directement, uniquement via DataSource. Une façon consiste à créer FakeDataSource en renvoyant les données de test.

S'il s'agit d'un test de bout en bout, vous pouvez simplement utiliser la base de données en mémoire. Ajoutez vos données de test avant de l'appeler. Exemple: https://medium.com/exploring-Android/android-architecture-components-testing-your-room-dao-classes-e06e1c9a1535

0
Deividas Strioga