Как я могу выполнить модульное тестирование пейджинга 3 (PagingSource)?

Недавно Google анонсировала новую библиотеку Paging 3, первую библиотеку Kotlin, поддержку сопрограмм, Flow и т. д.

Я играл с codelab. они предоставляют, но, похоже, пока нет никакой поддержки для тестирования, я также проверил документация. Они ничего не упомянули о тестировании, поэтому, например, я хотел протестировать этот PagingSource:

 class GithubPagingSource(private val service: GithubService,
                     private val query: String) : PagingSource<Int, Repo>() {

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {
    //params.key is null in loading first page in that case we would use constant GITHUB_STARTING_PAGE_INDEX
    val position = params.key ?: GITHUB_STARTING_PAGE_INDEX
    val apiQuery = query + IN_QUALIFIER
    return try {
        val response = service.searchRepos(apiQuery, position, params.loadSize)
        val data = response.items
        LoadResult.Page(
                        data,
                        if (position == GITHUB_STARTING_PAGE_INDEX) null else position - 1,
                        if (data.isEmpty()) null else position + 1)
    }catch (IOEx: IOException){
        Log.d("GithubPagingSource", "Failed to load pages, IO Exception: ${IOEx.message}")
        LoadResult.Error(IOEx)
    }catch (httpEx: HttpException){
        Log.d("GithubPagingSource", "Failed to load pages, http Exception code: ${httpEx.code()}")
        LoadResult.Error(httpEx)
    }
  }
}  

Итак, как я могу проверить это, кто-нибудь может мне помочь??


person MR3YY    schedule 19.06.2020    source источник
comment
Я хотел бы увидеть некоторые обновления по этому поводу   -  person Tiago Dávila    schedule 24.08.2020
comment
Я ответил на это в другом сообщении stackoverflow.com/a/67548760/164005. Проверьте это, чтобы увидеть, помогает ли это.   -  person Robert    schedule 16.05.2021


Ответы (4)


В настоящее время у меня есть аналогичный опыт, когда я обнаружил, что библиотека подкачки на самом деле не предназначена для тестирования. Я уверен, что Google сделает ее более пригодной для тестирования, когда она станет более зрелой библиотекой.

Я смог написать тест для PagingSource. Я использовал плагин RxJava 3 и mockito-kotlin, но общая идея теста должна быть воспроизводимо с версией API Coroutines и большинством сред тестирования.

class ItemPagingSourceTest {

    private val itemList = listOf(
            Item(id = "1"),
            Item(id = "2"),
            Item(id = "3")
    )

    private lateinit var source: ItemPagingSource

    private val service: ItemService = mock()

    @Before
    fun `set up`() {
        source = ItemPagingSource(service)
    }

    @Test
    fun `getItems - should delegate to service`() {
        val onSuccess: Consumer<LoadResult<Int, Item>> = mock()
        val onError: Consumer<Throwable> = mock()
        val params: LoadParams<Int> = mock()

        whenever(service.getItems(1)).thenReturn(Single.just(itemList))
        source.loadSingle(params).subscribe(onSuccess, onError)

        verify(service).getItems(1)
        verify(onSuccess).accept(LoadResult.Page(itemList, null, 2))
        verifyZeroInteractions(onError)
    }
}

Это не идеально, поскольку verify(onSuccess).accept(LoadResult.Page(itemList, null, 2)) полагается на то, что LoadResult.Page является data class, что можно сравнить по значениям его свойств. Но это тест PagingSource.

person Adrian Czuczka    schedule 24.01.2021

У меня есть решение, но я не думаю, что это хорошая идея для тестирования пейджинга v3. Все мои тесты для подкачки v3 работают на инструментальном тестировании, а не на локальном модульном тестировании, потому что, если я использую тот же метод в локальном тесте (тоже с robolectrict), он все равно не работает.

Итак, это мой тестовый пример, я использую mockwebserver для имитации и подсчета сетевого запроса, который должен быть равен моему ожидаемому

@RunWith(AndroidJUnit4::class)
@SmallTest
class SearchMoviePagingTest {
    private lateinit var recyclerView: RecyclerView
    private val query = "A"
    private val totalPage = 4

    private val service: ApiService by lazy {
        Retrofit.Builder()
                .baseUrl("http://localhost:8080")
                .addConverterFactory(GsonConverterFactory.create())
                .build().create(ApiService::class.java)
    }

    private val mappingCountCallHandler: HashMap<Int, Int> = HashMap<Int, Int>().apply {
        for (i in 0..totalPage) {
            this[i] = 0
        }
    }

    private val adapter: RecyclerTestAdapter<MovieItemResponse> by lazy {
        RecyclerTestAdapter()
    }

    private lateinit var pager: Flow<PagingData<MovieItemResponse>>

    private lateinit var mockWebServer: MockWebServer

    private val context: Context
        get() {
            return InstrumentationRegistry.getInstrumentation().targetContext
        }

    @Before
    fun setup() {
        mockWebServer = MockWebServer()
        mockWebServer.start(8080)

        recyclerView = RecyclerView(context)
        recyclerView.adapter = adapter

        mockWebServer.dispatcher = SearchMoviePagingDispatcher(context, ::receiveCallback)
        pager = Pager(
                config = PagingConfig(
                        pageSize = 20,
                        prefetchDistance = 3, // distance backward to get pages
                        enablePlaceholders = false,
                        initialLoadSize = 20
                ),
                pagingSourceFactory = { SearchMoviePagingSource(service, query) }
        ).flow
    }

    @After
    fun tearDown() {
        mockWebServer.dispatcher.shutdown()
        mockWebServer.shutdown()
    }

    @Test
    fun should_success_get_data_and_not_retrieve_anymore_page_if_not_reached_treshold() {
        runBlocking {
            val job = executeLaunch(this)
            delay(1000)
            adapter.forcePrefetch(10)
            delay(1000)

            Assert.assertEquals(1, mappingCountCallHandler[1])
            Assert.assertEquals(0, mappingCountCallHandler[2])
            Assert.assertEquals(20, adapter.itemCount)
            job.cancel()
        }
    }

....
    private fun executeLaunch(coroutineScope: CoroutineScope) = coroutineScope.launch {
        val res = pager.cachedIn(this)
        res.collectLatest {
            adapter.submitData(it)
        }
    }

    private fun receiveCallback(reqPage: Int) {
        val prev = mappingCountCallHandler[reqPage]!!
        mappingCountCallHandler[reqPage] = prev + 1
    }
}

#cmiiw пожалуйста :)

person Alexzander Purwoko    schedule 21.04.2021

Я только что столкнулся с тем же вопросом, и вот ответ:

Шаг 1 — создать макет.

@OptIn(ExperimentalCoroutinesApi::class)
class SubredditPagingSourceTest {
  private val postFactory = PostFactory()
  private val mockPosts = listOf(
    postFactory.createRedditPost(DEFAULT_SUBREDDIT),
    postFactory.createRedditPost(DEFAULT_SUBREDDIT),
    postFactory.createRedditPost(DEFAULT_SUBREDDIT)
  )
  private val mockApi = MockRedditApi().apply {
    mockPosts.forEach { post -> addPost(post) }
  }
}

Шаг 2 — модульное тестирование основного метода метода PageSource, load:

@Test
// Since load is a suspend function, runBlockingTest is used to ensure that it
// runs on the test thread.
fun loadReturnsPageWhenOnSuccessfulLoadOfItemKeyedData() = runBlockingTest {
  val pagingSource = ItemKeyedSubredditPagingSource(mockApi, DEFAULT_SUBREDDIT)
  assertEquals(
    expected = Page(
      data = listOf(mockPosts[0], mockPosts[1]),
      prevKey = mockPosts[0].name,
      nextKey = mockPosts[1].name
    ),
    actual = pagingSource.load(
      Refresh(
        key = null,
        loadSize = 2,
        placeholdersEnabled = false
      )
    ),
  )
}
person Cool    schedule 20.06.2021

Поток сопрограмм Kotlin

Вы можете использовать локальные тесты JUnit и установить TestCoroutineDispatcher< /a> до и после выполнения тестов. Затем вызовите методы, испускающие поток Kotlin PagingSource, чтобы просмотреть полученные данные в локальной тестовой среде и сравнить их с тем, что вы ожидаете.

Расширение теста JUnit 5 не требуется. Диспетчеры просто нужно установить и очистить до и после каждого теста, чтобы наблюдать Coroutines в тестовой среде по сравнению с системой Android.

@ExperimentalCoroutinesApi
class FeedViewTestExtension : BeforeEachCallback, AfterEachCallback, ParameterResolver {

    override fun beforeEach(context: ExtensionContext?) {
        // Set TestCoroutineDispatcher.
        Dispatchers.setMain(context?.root
                ?.getStore(TEST_COROUTINE_DISPATCHER_NAMESPACE)
                ?.get(TEST_COROUTINE_DISPATCHER_KEY, TestCoroutineDispatcher::class.java)!!)
    }

    override fun afterEach(context: ExtensionContext?) {
        // Reset TestCoroutineDispatcher.
        Dispatchers.resetMain()
        context?.root
                ?.getStore(TEST_COROUTINE_DISPATCHER_NAMESPACE)
                ?.get(TEST_COROUTINE_DISPATCHER_KEY, TestCoroutineDispatcher::class.java)!!
                .cleanupTestCoroutines()
        context.root
                ?.getStore(TEST_COROUTINE_SCOPE_NAMESPACE)
                ?.get(TEST_COROUTINE_SCOPE_KEY, TestCoroutineScope::class.java)!!
                .cleanupTestCoroutines()
    }

    ...
}

Вы можете увидеть локальные тесты JUnit 5 в примере приложения Coinverse для пейджинга 2 в разделе app/ src/test/java/app/coinverse/feedViewModel/FeedViewTest.

Отличие для пейджинга 3 заключается в том, что вам не нужно устанавливать исполнителей LiveData, так как Kotlin Flow может возвращать PagingData.

person Adam Hurwitz    schedule 21.09.2020
comment
Спасибо за полезное объяснение, но моя проблема на самом деле не в том, как тестировать потоки или сопрограммы. моя проблема в том, что нет надежного надежного решения для проверки этого PagingSource, например, например, FakePagingSource, предоставленного библиотекой. При этом я полагаюсь на макеты каждый раз, когда мне нужно протестировать этот PagingSource, что не всегда является хорошим решением, по крайней мере для меня, из-за таких вещей, как совмещение реализации с тестовым кодом. - person MR3YY; 21.09.2020
comment
Спасибо, что разъяснили @MR3YY. Мое решение в приведенном выше примере кода в значительной степени зависит от имитации и возврата имитированной версии PagindData. Библиотека пейджинга 3 все еще находится на ранней стадии разработки, поэтому я уверен, что команда разработчиков Android еще примет участие в тестировании. - person Adam Hurwitz; 21.09.2020