You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何对Google Paging 3的PagingSource进行单元测试?

嘿,我完全理解你的困惑——Paging 3的官方文档和codelab确实没怎么覆盖单元测试这块,但其实咱们可以通过模拟网络服务的方式,轻松对GithubPagingSource进行全面测试。下面我会一步步带你实现不同场景的测试用例:

第一步:准备测试依赖

首先确保你的测试依赖里包含这些库,用来处理协程、模拟对象和Paging相关测试:

testImplementation "junit:junit:4.13.2"
testImplementation "io.mockk:mockk:1.13.8" // 用MockK模拟GithubService,你也可以替换为Mockito
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3" // 支持测试协程挂起函数
testImplementation "androidx.paging:paging-common-test:3.2.1" // 可选,提供Paging测试辅助工具

第二步:编写测试类与核心测试用例

我们会针对成功加载、异常情况、空数据等场景编写测试,核心思路是模拟GithubService的行为,然后验证GithubPagingSource.load()的返回结果是否符合预期。

先定义测试类和基础变量:

import androidx.paging.PagingSource
import androidx.paging.PagingSource.LoadResult
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs

// 请根据你实际的数据类结构调整以下定义
data class Repo(val id: Int, val name: String)
data class SearchResponse(val items: List<Repo>)

class GithubPagingSourceTest {
    private val mockGithubService = mockk<GithubService>()
    private val testSearchQuery = "android"
    private val GITHUB_STARTING_PAGE_INDEX = 1
    private val IN_QUALIFIER = "in:name,description"

测试用例1:成功加载第一页

测试首次加载(params.key为null)时,是否返回正确的LoadResult.Page,包含数据、正确的prevKey(null)和nextKey(下一页页码):

@Test
    fun `load first page successfully returns Page with correct keys`() = runTest {
        // 1. 准备模拟数据
        val mockRepos = listOf(
            Repo(id = 1, name = "AndroidRepo1"),
            Repo(id = 2, name = "AndroidRepo2")
        )
        val mockSearchResponse = SearchResponse(items = mockRepos)

        // 2. 模拟GithubService的searchRepos方法返回成功结果
        coEvery {
            mockGithubService.searchRepos(
                query = "$testSearchQuery$IN_QUALIFIER",
                page = GITHUB_STARTING_PAGE_INDEX,
                perPage = 20
            )
        } returns mockSearchResponse

        // 3. 创建PagingSource实例并调用load方法
        val pagingSource = GithubPagingSource(mockGithubService, testSearchQuery)
        val loadResult = pagingSource.load(
            PagingSource.LoadParams.Refresh(
                key = null, // 首次加载key为null
                loadSize = 20,
                placeholdersEnabled = false
            )
        )

        // 4. 验证结果是否符合预期
        val expectedPage = LoadResult.Page(
            data = mockRepos,
            prevKey = null, // 第一页没有上一页
            nextKey = GITHUB_STARTING_PAGE_INDEX + 1 // 有数据,下一页是2
        )
        assertEquals(expectedPage, loadResult)
    }

测试用例2:成功加载后续页

测试加载第二页时,prevKey和nextKey是否正确:

@Test
    fun `load subsequent page returns Page with correct prev and next keys`() = runTest {
        val targetPage = 2
        val mockRepos = listOf(Repo(id = 3, name = "AndroidRepo3"))
        val mockSearchResponse = SearchResponse(items = mockRepos)

        coEvery {
            mockGithubService.searchRepos(
                query = "$testSearchQuery$IN_QUALIFIER",
                page = targetPage,
                perPage = 20
            )
        } returns mockSearchResponse

        val pagingSource = GithubPagingSource(mockGithubService, testSearchQuery)
        val loadResult = pagingSource.load(
            PagingSource.LoadParams.Append(
                key = targetPage,
                loadSize = 20,
                placeholdersEnabled = false
            )
        )

        val expectedPage = LoadResult.Page(
            data = mockRepos,
            prevKey = targetPage - 1, // 上一页是1
            nextKey = targetPage + 1 // 有数据,下一页是3
        )
        assertEquals(expectedPage, loadResult)
    }

测试用例3:捕获IO异常

测试当网络请求抛出IOException时,是否返回LoadResult.Error:

@Test
    fun `load returns Error result when IOException occurs`() = runTest {
        val mockIOException = IOException("Network connection failed")

        // 模拟service抛出IO异常
        coEvery {
            mockGithubService.searchRepos(any(), any(), any())
        } throws mockIOException

        val pagingSource = GithubPagingSource(mockGithubService, testSearchQuery)
        val loadResult = pagingSource.load(
            PagingSource.LoadParams.Refresh(null, 20, false)
        )

        // 验证结果是Error类型,且异常匹配
        assertIs<LoadResult.Error>(loadResult)
        assertEquals(mockIOException, loadResult.throwable)
    }

测试用例4:捕获Http异常

测试当请求返回错误状态码(比如404)时,是否正确返回LoadResult.Error:

@Test
    fun `load returns Error result when HttpException occurs`() = runTest {
        // 模拟一个404的HttpException
        val mockHttpException = retrofit2.HttpException(
            okhttp3.Response.Builder()
                .code(404)
                .message("Not Found")
                .request(okhttp3.Request.Builder().url("https://api.github.com/search/repos").build())
                .protocol(okhttp3.Protocol.HTTP_1_1)
                .build()
        )

        coEvery {
            mockGithubService.searchRepos(any(), any(), any())
        } throws mockHttpException

        val pagingSource = GithubPagingSource(mockGithubService, testSearchQuery)
        val loadResult = pagingSource.load(
            PagingSource.LoadParams.Refresh(null, 20, false)
        )

        assertIs<LoadResult.Error>(loadResult)
        assertEquals(mockHttpException, loadResult.throwable)
    }

测试用例5:加载空数据时nextKey为null

测试当接口返回空列表时,nextKey是否为null(表示没有更多数据):

@Test
    fun `load returns Page with null nextKey when no data is returned`() = runTest {
        val mockEmptyResponse = SearchResponse(items = emptyList())

        coEvery {
            mockGithubService.searchRepos(
                "$testSearchQuery$IN_QUALIFIER",
                GITHUB_STARTING_PAGE_INDEX,
                20
            )
        } returns mockEmptyResponse

        val pagingSource = GithubPagingSource(mockGithubService, testSearchQuery)
        val loadResult = pagingSource.load(
            PagingSource.LoadParams.Refresh(null, 20, false)
        )

        val expectedPage = LoadResult.Page(
            data = emptyList(),
            prevKey = null,
            nextKey = null // 空数据,没有下一页
        )
        assertEquals(expectedPage, loadResult)
    }
}

关键说明

  • runTest:来自kotlinx-coroutines-test,它提供了测试专用的协程作用域,让我们可以直接调用挂起函数,无需手动管理Job或Dispatcher。
  • coEvery:MockK中用于模拟挂起函数的API,因为GithubService.searchRepos是挂起函数,所以要用coEvery而不是普通的every。
  • LoadParams类型:Refresh对应首次加载或下拉刷新,Append对应加载下一页,Prepend对应加载上一页——你可以根据需要补充Prepend场景的测试。

这样就能全面覆盖GithubPagingSource的核心逻辑啦!

内容的提问来源于stack exchange,提问作者MR3YY

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.08 07:07:44