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

