![](/img/trans.png)
[英]How to Unit Test a Room Dao Query that Returns a PagingSource From Paging 3
[英]How can I Unit test Paging 3(PagingSource)?
谷歌最近宣布了新的 Paging 3 庫、Kotlin-first 庫、支持協程和 Flow 等。
我玩過他們提供的代碼實驗室,但似乎還沒有任何測試支持,我還檢查了文檔。 他們沒有提到任何關於測試的內容,所以例如我想對這個 PagingSource 進行單元測試:
class GithubPagingSource(private val service: GithubService,
private val query: String) : PagingSource<Int, Repo>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {
//params.key is null in loading first page in that case we would use constant GITHUB_STARTING_PAGE_INDEX
val position = params.key ?: GITHUB_STARTING_PAGE_INDEX
val apiQuery = query + IN_QUALIFIER
return try {
val response = service.searchRepos(apiQuery, position, params.loadSize)
val data = response.items
LoadResult.Page(
data,
if (position == GITHUB_STARTING_PAGE_INDEX) null else position - 1,
if (data.isEmpty()) null else position + 1)
}catch (IOEx: IOException){
Log.d("GithubPagingSource", "Failed to load pages, IO Exception: ${IOEx.message}")
LoadResult.Error(IOEx)
}catch (httpEx: HttpException){
Log.d("GithubPagingSource", "Failed to load pages, http Exception code: ${httpEx.code()}")
LoadResult.Error(httpEx)
}
}
}
那么,我該如何測試,有人可以幫助我嗎?
我目前有類似的經歷,發現分頁庫並不是真正設計為可測試的。 我敢肯定,一旦它成為一個更成熟的庫,Google 會使其更具可測試性。
我能夠為PagingSource
編寫測試。 我使用了 RxJava 3 插件和mockito-kotlin ,但是測試的總體思路應該可以使用 API 的 Coroutines 版本和大多數測試框架來重現。
class ItemPagingSourceTest {
private val itemList = listOf(
Item(id = "1"),
Item(id = "2"),
Item(id = "3")
)
private lateinit var source: ItemPagingSource
private val service: ItemService = mock()
@Before
fun `set up`() {
source = ItemPagingSource(service)
}
@Test
fun `getItems - should delegate to service`() {
val onSuccess: Consumer<LoadResult<Int, Item>> = mock()
val onError: Consumer<Throwable> = mock()
val params: LoadParams<Int> = mock()
whenever(service.getItems(1)).thenReturn(Single.just(itemList))
source.loadSingle(params).subscribe(onSuccess, onError)
verify(service).getItems(1)
verify(onSuccess).accept(LoadResult.Page(itemList, null, 2))
verifyZeroInteractions(onError)
}
}
這並不完美,因為verify(onSuccess).accept(LoadResult.Page(itemList, null, 2))
依賴於LoadResult.Page
作為data class
,可以通過其屬性值進行比較。 但它確實測試PagingSource
。
有一種方法可以使用 AsyncPagingDataDiffer
步驟 1. 創建 DiffCallback
class DiffFavoriteEventCallback: DiffUtil.ItemCallback<FavoriteEventUiModel>() { override fun areItemsTheSame( oldItem: FavoriteEventUiModel, newItem: FavoriteEventUiModel ): Boolean { return oldItem == newItem } override fun areContentsTheSame( oldItem: FavoriteEventUiModel, newItem: FavoriteEventUiModel ): Boolean { return oldItem == newItem } }
步驟 2. 創建 ListCallback
class NoopListCallback: ListUpdateCallback { override fun onChanged(position: Int, count: Int, payload: Any?) {} override fun onMoved(fromPosition: Int, toPosition: Int) {} override fun onInserted(position: Int, count: Int) {} override fun onRemoved(position: Int, count: Int) {} }
Step 3. 提交數據到diff並截圖
@Test fun WHEN_init_THEN_shouldGetEvents_AND_updateUiModel() { coroutineDispatcher.runBlockingTest { val eventList = listOf(FavoriteEvent(ID, TITLE, Date(1000), URL)) val pagingSource = PagingData.from(eventList) val captureUiModel = slot<PagingData<FavoriteEventUiModel>>() every { uiModelObserver.onChanged(capture(captureUiModel)) } answers {} coEvery { getFavoriteUseCase.invoke() } returns flowOf(pagingSource) viewModel.uiModel.observeForever(uiModelObserver) val differ = AsyncPagingDataDiffer( diffCallback = DiffFavoriteEventCallback(), updateCallback = NoopListCallback(), workerDispatcher = Dispatchers.Main ) val job = launch { viewModel.uiModel.observeForever { runBlocking { differ.submitData(it) } } } val result = differ.snapshot().items[0] assertEquals(result.id, ID) assertEquals(result.title, TITLE) assertEquals(result.url, URL) job.cancel() viewModel.uiModel.removeObserver(uiModelObserver) } }
文檔https://developer.android.com/reference/kotlin/androidx/paging/AsyncPagingDataDiffer
我有解決方案,但我認為這不是分頁 v3 測試的好主意。 我對分頁 v3 的所有測試都在進行儀器測試,而不是本地單元測試,這是因為如果我在本地測試中使用相同的方法(也使用 robolectrict),它仍然不起作用。
所以這是我的測試用例,我使用 mockwebserver 來模擬和計算必須等於我預期的網絡請求
@RunWith(AndroidJUnit4::class)
@SmallTest
class SearchMoviePagingTest {
private lateinit var recyclerView: RecyclerView
private val query = "A"
private val totalPage = 4
private val service: ApiService by lazy {
Retrofit.Builder()
.baseUrl("http://localhost:8080")
.addConverterFactory(GsonConverterFactory.create())
.build().create(ApiService::class.java)
}
private val mappingCountCallHandler: HashMap<Int, Int> = HashMap<Int, Int>().apply {
for (i in 0..totalPage) {
this[i] = 0
}
}
private val adapter: RecyclerTestAdapter<MovieItemResponse> by lazy {
RecyclerTestAdapter()
}
private lateinit var pager: Flow<PagingData<MovieItemResponse>>
private lateinit var mockWebServer: MockWebServer
private val context: Context
get() {
return InstrumentationRegistry.getInstrumentation().targetContext
}
@Before
fun setup() {
mockWebServer = MockWebServer()
mockWebServer.start(8080)
recyclerView = RecyclerView(context)
recyclerView.adapter = adapter
mockWebServer.dispatcher = SearchMoviePagingDispatcher(context, ::receiveCallback)
pager = Pager(
config = PagingConfig(
pageSize = 20,
prefetchDistance = 3, // distance backward to get pages
enablePlaceholders = false,
initialLoadSize = 20
),
pagingSourceFactory = { SearchMoviePagingSource(service, query) }
).flow
}
@After
fun tearDown() {
mockWebServer.dispatcher.shutdown()
mockWebServer.shutdown()
}
@Test
fun should_success_get_data_and_not_retrieve_anymore_page_if_not_reached_treshold() {
runBlocking {
val job = executeLaunch(this)
delay(1000)
adapter.forcePrefetch(10)
delay(1000)
Assert.assertEquals(1, mappingCountCallHandler[1])
Assert.assertEquals(0, mappingCountCallHandler[2])
Assert.assertEquals(20, adapter.itemCount)
job.cancel()
}
}
....
private fun executeLaunch(coroutineScope: CoroutineScope) = coroutineScope.launch {
val res = pager.cachedIn(this)
res.collectLatest {
adapter.submitData(it)
}
}
private fun receiveCallback(reqPage: Int) {
val prev = mappingCountCallHandler[reqPage]!!
mappingCountCallHandler[reqPage] = prev + 1
}
}
#cmiiw 請:)
我剛剛遇到同樣的問題,這是答案:
第 1 步是創建一個模擬。
@OptIn(ExperimentalCoroutinesApi::class)
class SubredditPagingSourceTest {
private val postFactory = PostFactory()
private val mockPosts = listOf(
postFactory.createRedditPost(DEFAULT_SUBREDDIT),
postFactory.createRedditPost(DEFAULT_SUBREDDIT),
postFactory.createRedditPost(DEFAULT_SUBREDDIT)
)
private val mockApi = MockRedditApi().apply {
mockPosts.forEach { post -> addPost(post) }
}
}
第二步是對PageSource
的核心方法, load
方法進行單元測試:
@Test
// Since load is a suspend function, runBlockingTest is used to ensure that it
// runs on the test thread.
fun loadReturnsPageWhenOnSuccessfulLoadOfItemKeyedData() = runBlockingTest {
val pagingSource = ItemKeyedSubredditPagingSource(mockApi, DEFAULT_SUBREDDIT)
assertEquals(
expected = Page(
data = listOf(mockPosts[0], mockPosts[1]),
prevKey = mockPosts[0].name,
nextKey = mockPosts[1].name
),
actual = pagingSource.load(
Refresh(
key = null,
loadSize = 2,
placeholdersEnabled = false
)
),
)
}
您可以使用 JUnit 本地測試並在測試運行之前和之后設置TestCoroutineDispatcher
。 然后,調用發出 PagingSource 的PagingSource
流的方法,在本地測試環境中觀察結果數據,與您的預期進行比較。
不需要 JUnit 5 測試擴展。 只需在每次測試之前和之后設置和清除調度程序,以便觀察測試環境中的協程與 Android 系統上的協程。
@ExperimentalCoroutinesApi
class FeedViewTestExtension : BeforeEachCallback, AfterEachCallback, ParameterResolver {
override fun beforeEach(context: ExtensionContext?) {
// Set TestCoroutineDispatcher.
Dispatchers.setMain(context?.root
?.getStore(TEST_COROUTINE_DISPATCHER_NAMESPACE)
?.get(TEST_COROUTINE_DISPATCHER_KEY, TestCoroutineDispatcher::class.java)!!)
}
override fun afterEach(context: ExtensionContext?) {
// Reset TestCoroutineDispatcher.
Dispatchers.resetMain()
context?.root
?.getStore(TEST_COROUTINE_DISPATCHER_NAMESPACE)
?.get(TEST_COROUTINE_DISPATCHER_KEY, TestCoroutineDispatcher::class.java)!!
.cleanupTestCoroutines()
context.root
?.getStore(TEST_COROUTINE_SCOPE_NAMESPACE)
?.get(TEST_COROUTINE_SCOPE_KEY, TestCoroutineScope::class.java)!!
.cleanupTestCoroutines()
}
...
}
您可以在app/src/test/java/app/coinverse/feedViewModel/FeedViewTest下的分頁 2 的Coinverse 示例應用程序中看到本地 JUnit 5 測試。
Paging 3 的不同之處在於您不需要設置 LiveData 執行器,因為 Kotlin Flow 可以返回PagingData
。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.