Android 如何为测试创建对象的页面列表?
我一直在使用Google的arch库,但有一件事使测试变得困难,那就是使用Android 如何为测试创建对象的页面列表?,android,kotlin,android-testing,android-architecture-components,android-paging,Android,Kotlin,Android Testing,Android Architecture Components,Android Paging,我一直在使用Google的arch库,但有一件事使测试变得困难,那就是使用PagedList 对于本例,我使用存储库模式并从API或网络返回详细信息 因此,在ViewModel中,我调用了这个接口方法: override fun getFoos(): Observable<PagedList<Foo>> 我希望能够通过测试设置这些返回页面列表的方法的返回。类似于 when(repository.getFoos()).thenReturn(Observable.just(
PagedList
对于本例,我使用存储库模式并从API或网络返回详细信息
因此,在ViewModel中,我调用了这个接口方法:
override fun getFoos(): Observable<PagedList<Foo>>
我希望能够通过测试设置这些返回页面列表的方法的返回。类似于
when(repository.getFoos()).thenReturn(Observable.just(TEST_PAGED_LIST_OF_FOOS)
两个问题:
这可能吗
如何创建页面列表
我的目标是以更端到端的方式进行验证(例如确保屏幕上显示正确的foo列表)。片段/活动/视图是从ViewModel观察页面列表的视图
无法将列表强制转换为页面列表
您不能直接创建页面列表,只能通过数据源创建。一种方法是创建FakeDataSource并返回测试数据李>
如果是端到端测试,您可以只使用内存数据库。在调用之前添加测试数据。例子:
实现这一点的简单方法是模拟页面列表。此功能将列表“转换”为页面列表(在这种情况下,我们使用的不是真正的页面列表,而只是模拟版本,如果您需要实现其他页面列表方法,请将它们添加到此功能中)
页面列表(列表:列表):页面列表{
val pagedList=Mockito.mock(pagedList::class.java)作为pagedList
Mockito.`when`(pagedList.get(ArgumentMatchers.anyInt())。然后{invocation->
val index=invocation.arguments.first()作为Int
列表[索引]
}
Mockito.when`(pagedList.size)。然后返回(list.size)
返回页面列表
}
第3页
分页3库提供了一个生成器方法
第2页
使用模拟的DataSource.Factory
将列表转换为页面列表
在本期中分享了这一点。为了使用Kotlin、JUnit 5、MockK和AssertJ库对ViewModel进行本地单元测试,我在中实现了mocked PagedList
为了观察页面列表中的实时数据,我使用了谷歌Android架构组件示例中的getOrAwaitValue
asPagedList
扩展函数在下面的示例测试内容viewmodeltest.kt中实现
PagedListTestUtil.kt
InstantExecutorExtension.kt
在使用LiveData时,JUnit5需要这样做,以确保观察者不在主线程上。下面是
我发现您的问题中缺少的是:您希望在测试中进行什么验证?@arekolek谢谢您的反馈…我用我试图验证的内容更新了问题@isuPatches是否更新了您实施的策略?我正在用Kotlin、JUnit5和Mock编写相同的测试。我很高兴在这里分享我的解决方案,一旦我得到了概念验证。别忘了先添加Mockito:testImplementation“org.Mockito:Mockito core:2.25.0”
你能将列表
投射到页面列表
@bsobat?@bsobat,这应该返回一个填充了数据的页面列表
,还是返回一个空的页面列表
,以避免测试失败?如果为空,它将无法用于测试应用程序如何处理页面列表
数据。@Hurwitz不,您不能cast@bsobat,查看上面使用MockK实现在编写本地JUnit 5测试时使用PagedList的行为。感谢您的深入了解!我有一个模拟页面列表,在本地JUnit 5测试中使用MockK。它是否与分页3库类似?好问题@Raghunandan!我还没有测试分页3库。如果您实施并发现更改,请在帖子上添加评论/编辑。
when(repository.getFoos()).thenReturn(Observable.just(TEST_PAGED_LIST_OF_FOOS)
fun <T> mockPagedList(list: List<T>): PagedList<T> {
val pagedList = Mockito.mock(PagedList::class.java) as PagedList<T>
Mockito.`when`(pagedList.get(ArgumentMatchers.anyInt())).then { invocation ->
val index = invocation.arguments.first() as Int
list[index]
}
Mockito.`when`(pagedList.size).thenReturn(list.size)
return pagedList
}
import android.database.Cursor
import androidx.paging.DataSource
import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
import androidx.room.RoomDatabase
import androidx.room.RoomSQLiteQuery
import androidx.room.paging.LimitOffsetDataSource
import io.mockk.every
import io.mockk.mockk
fun <T> List<T>.asPagedList() = LivePagedListBuilder<Int, T>(createMockDataSourceFactory(this),
Config(enablePlaceholders = false,
prefetchDistance = 24,
pageSize = if (size == 0) 1 else size))
.build().getOrAwaitValue()
private fun <T> createMockDataSourceFactory(itemList: List<T>): DataSource.Factory<Int, T> =
object : DataSource.Factory<Int, T>() {
override fun create(): DataSource<Int, T> = MockLimitDataSource(itemList)
}
private val mockQuery = mockk<RoomSQLiteQuery> {
every { sql } returns ""
}
private val mockDb = mockk<RoomDatabase> {
every { invalidationTracker } returns mockk(relaxUnitFun = true)
}
class MockLimitDataSource<T>(private val itemList: List<T>) : LimitOffsetDataSource<T>(mockDb, mockQuery, false, null) {
override fun convertRows(cursor: Cursor?): MutableList<T> = itemList.toMutableList()
override fun countItems(): Int = itemList.count()
override fun isInvalid(): Boolean = false
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<T>) { /* Not implemented */ }
override fun loadRange(startPosition: Int, loadCount: Int) =
itemList.subList(startPosition, startPosition + loadCount).toMutableList()
override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<T>) {
callback.onResult(itemList, 0)
}
}
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
/**
* Gets the value of a [LiveData] or waits for it to have one, with a timeout.
*
* Use this extension from host-side (JVM) tests. It's recommended to use it alongside
* `InstantTaskExecutorRule` or a similar mechanism to execute tasks synchronously.
*/
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data = o
latch.countDown()
this@getOrAwaitValue.removeObserver(this)
}
}
this.observeForever(observer)
afterObserve.invoke()
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
this.removeObserver(observer)
throw TimeoutException("LiveData value was never set.")
}
@Suppress("UNCHECKED_CAST")
return data as T
}
...
import androidx.paging.PagedList
import com.google.firebase.Timestamp
import io.mockk.*
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
@ExtendWith(InstantExecutorExtension::class)
class ContentViewModelTest {
val timestamp = getTimeframe(DAY)
@BeforeAll
fun beforeAll() {
mockkObject(ContentRepository)
}
@BeforeEach
fun beforeEach() {
clearAllMocks()
}
@AfterAll
fun afterAll() {
unmockkAll()
}
@Test
fun `Feed Load`() {
val content = Content("85", 0.0, Enums.ContentType.NONE, Timestamp.now(), "",
"", "", "", "", "", "", MAIN,
0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0)
every {
getMainFeedList(any(), any())
} returns liveData {
emit(Lce.Content(
ContentResult.PagedListResult(
pagedList = liveData {emit(listOf(content).asPagedList())},
errorMessage = ""))
}
val contentViewModel = ContentViewModel(ContentRepository)
contentViewModel.processEvent(ContentViewEvent.FeedLoad(MAIN, DAY, timestamp, false))
assertThat(contentViewModel.feedViewState.getOrAwaitValue().contentList.getOrAwaitValue()[0])
.isEqualTo(content)
assertThat(contentViewModel.feedViewState.getOrAwaitValue().toolbar).isEqualTo(
ToolbarState(
visibility = GONE,
titleRes = app_name,
isSupportActionBarEnabled = false))
verify {
getMainFeedList(any(), any())
}
confirmVerified(ContentRepository)
}
}
import androidx.arch.core.executor.ArchTaskExecutor
import androidx.arch.core.executor.TaskExecutor
import org.junit.jupiter.api.extension.AfterEachCallback
import org.junit.jupiter.api.extension.BeforeEachCallback
import org.junit.jupiter.api.extension.ExtensionContext
class InstantExecutorExtension : BeforeEachCallback, AfterEachCallback {
override fun beforeEach(context: ExtensionContext?) {
ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() {
override fun executeOnDiskIO(runnable: Runnable) = runnable.run()
override fun postToMainThread(runnable: Runnable) = runnable.run()
override fun isMainThread(): Boolean = true
})
}
override fun afterEach(context: ExtensionContext?) {
ArchTaskExecutor.getInstance().setDelegate(null)
}
}