如何对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
相关产品推荐
相关产品推荐

