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

Android Paging3中如何测试返回Flow<PagingData<Movie>>的getAll()方法

测试返回 Flow<PagingData> 的Repository方法的问题

我在Repository类中有一个名为getAll()的方法,请问该如何测试这个方法?getAll()方法的实现如下:

fun getAll(): Flow<PagingData<Movie>> {
    return Pager(
        config = PagingConfig(enablePlaceholders = false, pageSize = 20),
        pagingSourceFactory = { MoviesPagingSource(movieApiService) }
    ).flow
}

MoviesPagingSource类的实现如下:

import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.oazisn.moviecatalog.data.local.entity.Movie

class MoviesPagingSource(
    private val service: MovieApiService,
) : PagingSource<Int, Movie>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Movie> {
        val page = params.key ?: 1
        return try {
            val response = service.getPopular(page)
            val movies = ArrayList<Movie>()
            response.results?.forEach { data ->
                movies.add(
                    Movie(
                        "https://image.tmdb.org/t/p/original${data?.posterPath}",
                        data?.title ?: "-",
                        data?.voteAverage ?: 0.0,
                        data?.id ?: 0
                    )
                )
            }
            LoadResult.Page(
                data = movies,
                prevKey = if (page == 1) null else page - 1,
                nextKey = if (page == response.totalPages) null else page + 1
            )
        } catch (exception: Exception) {
            LoadResult.Error(exception)
        }
    }

    override fun getRefreshKey(state: PagingState<Int, Movie>): Int? {
        // 尝试从prevKey或nextKey中查找距离anchorPosition最近的页密钥,需要处理空值情况:
        // * prevKey == null -> anchorPage为第一页
        // * nextKey == null -> anchorPage为最后一页
        // * prevKey和nextKey均为null时,anchorPage为初始页,直接返回null
        return state.anchorPosition?.let { anchorPosition ->
            val anchorPage = state.closestPageToPosition(anchorPosition)
            anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
        }
    }
}

PagingData是Android Paging v3库中的类。

我目前尝试编写的单元测试代码如下:

@Test
fun getAll() {
    testCoroutineRule.runBlockingTest {
        val dummyList = listOf(
            MovieResultItem(
                posterPath = "/6Wdl9N6dL0Hi0T1qJLWSz6gMLbd.jpg",
                title = "Mortal Kombat",
                voteAverage = 7.9,
                id = 460465
            ),
            MovieResultItem(
                posterPath = "/pgqgaUx1cJb5oZQQ5v0tNARCeBp.jpg",
                title = "Godzilla vs. Kong",
                voteAverage = 8.2,
                id = 399566
            ),
            MovieResultItem(
                posterPath = "/oBgWY00bEFeZ9N25wWVyuQddbAo.jpg",
                title = "Nobody",
                voteAverage = 8.5,
                id = 615457
            ),
            MovieResultItem(
                posterPath = "/tnAuB8q5vv7Ax9UAEje5Xi4BXik.jpg",
                title = "Zack Snyder's Justice League",
                voteAverage = 8.5,
                id = 791373
            )
        )
        val dummyObject = ListBaseResponse(
            results = dummyList
        )

        `when`(movieApiService.getPopular(1)).thenReturn(dummyObject)

        val movies = ArrayList<Movie>()
        dummyObject.results?.forEach { data ->
            movies.add(
                Movie(
                    "https://image.tmdb.org/t/p/original${data?.posterPath}",
                    data?.title ?: "-",
                    data?.voteAverage ?: 0.0,
                    data?.id ?: 0
                )
            )
        }

        val response = repository.getAll()

        val repoResponseAsLiveData = response.asLiveData()
        assertEquals(movies, repoResponseAsLiveData.value)

    }
}

测试运行后得到如下错误信息:

java.lang.AssertionError: 
Expected :[Movie(poster=https://image.tmdb.org/t/p/original/6Wdl9N6dL0Hi0T1qJLWSz6gMLbd.jpg, title=Mortal Kombat, rating=7.9, id=460465, rated=, duration=, genre=, releaseDate=, desc=, director=, writers=, stars=, creator=), Movie(poster=https://image.tmdb.org/t/p/original/pgqgaUx1cJb5oZQQ5v0tNARCeBp.jpg, title=Godzilla vs. Kong, rating=8.2, id=399566, rated=, duration=, genre=, releaseDate=, desc=, director=, writers=, stars=, creator=), Movie(poster=https://image.tmdb.org/t/p/original/oBgWY00bEFeZ9N25wWVyuQddbAo.jpg, title=Nobody, rating=8.5, id=615457, rated=, duration=, genre=, releaseDate=, desc=, director=, writers=, stars=, creator=), Movie(poster=https://image.tmdb.org/t/p/original/tnAuB8q5vv7Ax9UAEje5Xi4BXik.jpg, title=Zack Snyder's Justice League, rating=8.5, id=791373, rated=, duration=, genre=, releaseDate=, desc=, director=, writers=, stars=, creator=)]
Actual   :null

问题原因

Flow<PagingData>属于冷流,只有被主动收集并触发Paging加载流程时,才会真正调用PagingSource发起网络请求拉取数据。你使用asLiveData()转换后直接读取value,既没有完成LiveData的订阅初始化,也没有触发Paging的异步加载,因此拿到的始终是null。
另外你的mock数据存在缺陷:构造ListBaseResponse时没有传入totalPages字段,会导致MoviesPagingSource计算nextKey时触发非预期的第二页加载请求,即便修复了收集逻辑也会出现空指针或加载失败。

解决方案

  • 引入Paging官方测试依赖:
testImplementation "androidx.paging:paging-testing:3.2.1"
  • 修正mock数据,补全totalPages字段,避免额外分页加载。
  • 改用Paging测试库提供的test()方法自动触发加载、扁平化收集数据后再做断言,替换掉原来的asLiveData()逻辑。

修正后的测试代码如下:

@Test
fun getAll() = runTest {
    val dummyList = listOf(
        MovieResultItem(
            posterPath = "/6Wdl9N6dL0Hi0T1qJLWSz6gMLbd.jpg",
            title = "Mortal Kombat",
            voteAverage = 7.9,
            id = 460465
        ),
        MovieResultItem(
            posterPath = "/pgqgaUx1cJb5oZQQ5v0tNARCeBp.jpg",
            title = "Godzilla vs. Kong",
            voteAverage = 8.2,
            id = 399566
        ),
        MovieResultItem(
            posterPath = "/oBgWY00bEFeZ9N25wWVyuQddbAo.jpg",
            title = "Nobody",
            voteAverage = 8.5,
            id = 615457
        ),
        MovieResultItem(
            posterPath = "/tnAuB8q5vv7Ax9UAEje5Xi4BXik.jpg",
            title = "Zack Snyder's Justice League",
            voteAverage = 8.5,
            id = 791373
        )
    )
    // 补全totalPages,标记当前为最后一页,避免加载下一页
    val dummyObject = ListBaseResponse(
        results = dummyList,
        totalPages = 1
    )

    `when`(movieApiService.getPopular(1)).thenReturn(dummyObject)

    // 构造期望的本地Movie实体列表
    val expectedMovies = dummyObject.results!!.map { data ->
        Movie(
            poster = "https://image.tmdb.org/t/p/original${data.posterPath}",
            title = data.title,
            rating = data.voteAverage,
            id = data.id
        )
    }

    // 调用Repository方法,用测试工具自动收集PagingData
    repository.getAll()
        .test()
        .awaitData() // 等待第一页数据加载完成
        .assertData { actualMovies ->
            assertEquals(expectedMovies, actualMovies)
        }
}

注意事项

  • 新版协程测试库推荐使用runTest替代已废弃的runBlockingTest,如果保留原有的testCoroutineRule,也可以直接替换规则内的执行逻辑,效果一致。
  • paging-testing的test()方法会自动模拟Paging的加载流程,自动处理PagingData的增量更新,不需要手动处理Flow收集逻辑。
  • 所有mock的网络响应字段需要和PagingSource的读取逻辑完全匹配,否则会触发非预期的异常加载。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.22 09:36:02