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

如何为Android Paging Library编写分页网络请求单元测试

分页网络请求单元测试方案

针对你基于Android Paging Library实现的分页逻辑,我们可以分数据源类(ListDataSource)、仓库类(ListPagingRepository)、ViewModel方法三个层级编写单元测试,以下是具体实现步骤:

一、依赖准备

确保测试依赖中包含:

  • kotlinx-coroutines-test:处理协程测试场景
  • mockk(或Mockito):模拟NetworkService等依赖实例
  • androidx.paging:paging-testing:Paging库专属测试工具

二、测试ListDataSource

数据源是分页逻辑核心,重点测试load方法的不同场景:成功返回满页数据、返回不足页数据、网络异常。

示例代码(MockK+协程测试)

import app.cash.turbine.test
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class ListDataSourceTest {
    private val mockNetworkService = mockk<NetworkService>()
    private val testSearchKey = "test"
    private val dataSource = ListDataSource(mockNetworkService, testSearchKey)

    @Test
    fun `load first page returns valid Page with next key`() = runTest {
        // 模拟网络返回30条满页数据
        val mockResponse = mockk<ApiResponse> {
            every { response } returns mockk {
                every { list } returns List(30) { ListItem(id = it) }
            }
        }
        coEvery { mockNetworkService.getList(testSearchKey, 0, 30) } returns mockResponse

        val result = dataSource.load(LoadParams.Refresh(null, 30, false))
        
        // 验证返回结果符合预期
        assertTrue(result is LoadResult.Page)
        assertEquals(30, (result as LoadResult.Page).data.size)
        assertEquals(null, result.prevKey)
        assertEquals(1, result.nextKey)
    }

    @Test
    fun `load page with less than 30 items returns Page with null next key`() = runTest {
        // 模拟返回20条不足页数据
        val mockResponse = mockk<ApiResponse> {
            every { response } returns mockk {
                every { list } returns List(20) { ListItem(id = it) }
            }
        }
        coEvery { mockNetworkService.getList(testSearchKey, 1, 30) } returns mockResponse

        val result = dataSource.load(LoadParams.Append(1, 30, false))
        
        assertTrue(result is LoadResult.Page)
        assertEquals(20, (result as LoadResult.Page).data.size)
        assertEquals(0, result.prevKey)
        assertEquals(null, result.nextKey)
    }

    @Test
    fun `load throws exception returns Error result`() = runTest {
        // 模拟网络异常
        val testException = RuntimeException("Network error")
        coEvery { mockNetworkService.getList(testSearchKey, any(), any()) } throws testException

        val result = dataSource.load(LoadParams.Refresh(null, 30, false))
        
        assertTrue(result is LoadResult.Error)
        assertEquals(testException, (result as LoadResult.Error).throwable)
    }
}

三、测试ListPagingRepository

仓库类主要测试Pager的初始化逻辑,以及数据源创建是否正常。

示例代码

import io.mockk.mockk
import org.junit.Test
import kotlin.test.assertTrue
import kotlin.test.assertFalse

class ListPagingRepositoryTest {
    private val mockNetworkService = mockk<NetworkService>()
    private val repository = ListPagingRepository(mockNetworkService)

    @Test
    fun `createSource and createPager initializes pager correctly`() {
        assertFalse(repository.isPagerInitialized())
        
        repository.createSource("test")
        repository.createPager()
        
        assertTrue(repository.isPagerInitialized())
        // 验证Pager配置符合预期
        val pager = repository.getListPager()
        assertEquals(15, pager.config.pageSize)
        assertEquals(false, pager.config.enablePlaceholders)
    }
}

四、测试ViewModel的getPaginatedList方法

重点测试Flow是否正确发射数据,以及数据映射逻辑是否正常。

示例代码

import app.cash.turbine.test
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Test
import kotlin.test.assertEquals

class ListViewModelTest {
    private val mockRepository = mockk<ListPagingRepository>(relaxed = true)
    private val mockMapper = mockk<ListMapper>(relaxed = true)
    private val viewModel = ListViewModel(mockRepository, mockMapper)

    @Test
    fun `getPaginatedList returns mapped PagingData flow when pager is initialized`() = runTest {
        // 模拟仓库返回的PagingData Flow
        val testPagingData = PagingData.from(listOf(ListItem(id = 1)))
        val mockPager = mockk<Pager<Int, ListItem>> {
            every { flow } returns kotlinx.coroutines.flow.flow { emit(testPagingData) }
        }
        coEvery { mockRepository.isPagerInitialized() } returns true
        coEvery { mockRepository.getListPager() } returns mockPager
        
        // 模拟数据映射结果
        val mappedItem = MappedListItem(id = 1)
        coEvery { mockMapper.map(any<ListItem>()) } returns mappedItem

        viewModel.getPaginatedList("test").test {
            val emittedData = awaitItem()
            // 转换为列表验证映射结果
            val items = mutableListOf<MappedListItem>()
            emittedData.collect { items.add(it) }
            assertEquals(1, items.size)
            assertEquals(mappedItem.id, items[0].id)
            awaitComplete()
        }
    }

    @Test
    fun `getPaginatedList returns empty flow when pager is not initialized`() = runTest {
        coEvery { mockRepository.isPagerInitialized() } returns false

        viewModel.getPaginatedList("test").test {
            expectNoEvents()
        }
    }
}

关键测试要点

  • 协程处理:所有涉及suspend的测试用runTest包裹,简化协程测试流程
  • 边界场景覆盖:测试第一页、最后一页(无下一页)、网络异常、空数据等情况
  • 数据映射验证:确保ViewModel中的map操作正确转换数据
  • Pager配置验证:确认仓库创建的Pager使用了预期的PagingConfig

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 10:51:11