简体   繁体   English

如何对 Paging 3(PagingSource) 进行单元测试?

[英]How can I Unit test Paging 3(PagingSource)?

Google recently announced the new Paging 3 library, Kotlin-first library, Support for coroutines and Flow...etc.谷歌最近宣布了新的 Paging 3 库、Kotlin-first 库、支持协程和 Flow 等。

I played with the codelab they provide but it seems there's not any support yet for testing, I also checked documentation .我玩过他们提供的代码实验室,但似乎还没有任何测试支持,我还检查了文档 They didn't mention anything about testing, So For Example I wanted to unit test this PagingSource:他们没有提到任何关于测试的内容,所以例如我想对这个 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)
    }
  }
}  

So, How can I test this, is anyone can help me??那么,我该如何测试,有人可以帮助我吗?

I'm currently having a similar experience of finding out that the paging library isn't really designed to be testable.我目前有类似的经历,发现分页库并不是真正设计为可测试的。 I'm sure Google will make it more testable once it's a more mature library.我敢肯定,一旦它成为一个更成熟的库,Google 会使其更具可测试性。

I was able to write a test for PagingSource .我能够为PagingSource编写测试。 I used the RxJava 3 plugin and mockito-kotlin , but the general idea of the test should be reproducible with the Coroutines version of the API and most testing frameworks.我使用了 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)
    }
}

It's not perfect, since verify(onSuccess).accept(LoadResult.Page(itemList, null, 2)) relies on LoadResult.Page being a data class , which can be compared by the values of its properties.这并不完美,因为verify(onSuccess).accept(LoadResult.Page(itemList, null, 2))依赖于LoadResult.Page作为data class ,可以通过其属性值进行比较。 But it does test PagingSource .但它确实测试PagingSource

There is a way to do that with AsyncPagingDataDiffer有一种方法可以使用 AsyncPagingDataDiffer

Step 1. Create DiffCallback步骤 1. 创建 DiffCallback

 class DiffFavoriteEventCallback: DiffUtil.ItemCallback<FavoriteEventUiModel>() { override fun areItemsTheSame( oldItem: FavoriteEventUiModel, newItem: FavoriteEventUiModel ): Boolean { return oldItem == newItem } override fun areContentsTheSame( oldItem: FavoriteEventUiModel, newItem: FavoriteEventUiModel ): Boolean { return oldItem == newItem } }

Step 2. Create ListCallback步骤 2. 创建 ListCallback

 class NoopListCallback: ListUpdateCallback { override fun onChanged(position: Int, count: Int, payload: Any?) {} override fun onMoved(fromPosition: Int, toPosition: Int) {} override fun onInserted(position: Int, count: Int) {} override fun onRemoved(position: Int, count: Int) {} }

Step 3. Submit data to the differ and take the screenshot Step 3. 提交数据到diff并截图

 @Test fun WHEN_init_THEN_shouldGetEvents_AND_updateUiModel() { coroutineDispatcher.runBlockingTest { val eventList = listOf(FavoriteEvent(ID, TITLE, Date(1000), URL)) val pagingSource = PagingData.from(eventList) val captureUiModel = slot<PagingData<FavoriteEventUiModel>>() every { uiModelObserver.onChanged(capture(captureUiModel)) } answers {} coEvery { getFavoriteUseCase.invoke() } returns flowOf(pagingSource) viewModel.uiModel.observeForever(uiModelObserver) val differ = AsyncPagingDataDiffer( diffCallback = DiffFavoriteEventCallback(), updateCallback = NoopListCallback(), workerDispatcher = Dispatchers.Main ) val job = launch { viewModel.uiModel.observeForever { runBlocking { differ.submitData(it) } } } val result = differ.snapshot().items[0] assertEquals(result.id, ID) assertEquals(result.title, TITLE) assertEquals(result.url, URL) job.cancel() viewModel.uiModel.removeObserver(uiModelObserver) } }

Documentation https://developer.android.com/reference/kotlin/androidx/paging/AsyncPagingDataDiffer文档https://developer.android.com/reference/kotlin/androidx/paging/AsyncPagingDataDiffer

I have the solution, but i don't think this is the good idea for paging v3 testing.我有解决方案,但我认为这不是分页 v3 测试的好主意。 My all test for paging v3 is working on instrumentation testing, not local unit testing, this because if i put the same way method in local test (with robolectrict too) it still doesn't work.我对分页 v3 的所有测试都在进行仪器测试,而不是本地单元测试,这是因为如果我在本地测试中使用相同的方法(也使用 robolectrict),它仍然不起作用。

So this is my test case, I use the mockwebserver to mock and count the network request that must be equal to my expected所以这是我的测试用例,我使用 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 please:) #cmiiw 请:)

I just come across the same question, and here is the answer :我刚刚遇到同样的问题,这是答案

Step 1 is to create a mock.第 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) }
  }
}

Step 2 is to unit test the core method of PageSource , load method:第二步是对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
      )
    ),
  )
}

Kotlin Coroutines Flow Kotlin 协程流程

You can use JUnit local tests and set the TestCoroutineDispatcher before and after the tests run.您可以使用 JUnit 本地测试并在测试运行之前和之后设置TestCoroutineDispatcher Then, call the methods that emit the Kotlin Flow of the PagingSource to observe the resulting data in the local testing environment to compare with what you expect.然后,调用发出 PagingSource 的PagingSource流的方法,在本地测试环境中观察结果数据,与您的预期进行比较。

A JUnit 5 test extension is not required.不需要 JUnit 5 测试扩展。 The dispatchers just need to be set and cleared before and after each test in order to observe Coroutines in the test environment vs. on the Android system.只需在每次测试之前和之后设置和清除调度程序,以便观察测试环境中的协程与 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()
    }

    ...
}

You can see the local JUnit 5 tests in the Coinverse sample app for Paging 2 under app/src/test/java/app/coinverse/feedViewModel/FeedViewTest .您可以在app/src/test/java/app/coinverse/feedViewModel/FeedViewTest下的分页 2 的Coinverse 示例应用程序中看到本地 JUnit 5 测试。

The difference for Paging 3 is that you don't need to set LiveData executor's since Kotlin Flow can return PagingData . Paging 3 的不同之处在于您不需要设置 LiveData 执行器,因为 Kotlin Flow 可以返回PagingData

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 如何对从 Paging 3 返回 PagingSource 的 Room Dao 查询进行单元测试 - How to Unit Test a Room Dao Query that Returns a PagingSource From Paging 3 Paging3 不知道如何转换 PagingSource - Paging3 not sure how to convert PagingSource 如何在本地数据上正确使用带有 PagingDataAdapter 的自定义 PagingSource? - How can I correctly use custom PagingSource with PagingDataAdapter, on local data? 如何动态修改 PagingSource 类中的变量,Paging Library 3.0 - How to dynamically modify variables in PagingSource class, Paging Library 3.0 Android Jetpack Paging 3:带 Room 的 PagingSource - Android Jetpack Paging 3: PagingSource with Room Android:单元测试:如何使用SensorManager创建单元测试? - Android: Unit Test: How can I create unit test with SensorManager? 如何使用 Proto DataStore 进行单元测试? - How can I unit test with Proto DataStore? 如何为基于 cursor 的分页实现 PagingSource.getRefreshKey - Android Jetpack Paging 3 - How to implement PagingSource.getRefreshKey for cursor based pagination - Android Jetpack Paging 3 来自分页库 3 的 PagingSource,结果为回调 - PagingSource from paging library 3 with callback as result Paging3:在 Room DAO 中使用 PagingSource 作为返回类型时,“不确定如何将 Cursor 转换为此方法的返回类型” - Paging3: “Not sure how to convert a Cursor to this method's return type” when using PagingSource as return type in Room DAO
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM