Как создать PagedList объекта для тестов?
Я работаю с PagedList
от Google, но одна вещь, которая затрудняет тестирование, - это работа с PagedList
.
В этом примере я использую шаблон репозитория и возвращаю информацию из API или сети.
Поэтому в ViewModel я вызываю этот метод интерфейса:
override fun getFoos(): Observable<PagedList<Foo>>
Затем репозиторий будет использовать RxPagedListBuilder
для создания Observable
который имеет тип PagedList:
override fun getFoos(): Observable<PagedList<Foo>> =
RxPagedListBuilder(database.fooDao().selectAll(), PAGED_LIST_CONFIG).buildObservable()
Я хочу, чтобы тесты могли настраивать возврат из этих методов, которые возвращают PagedList<Foo>
. Что-то похожее
when(repository.getFoos()).thenReturn(Observable.just(TEST_PAGED_LIST_OF_FOOS)
Два вопроса:
- Это возможно?
- Как создать
PagedList<Foo>
?
Моя цель состоит в том, чтобы убедиться в более полном конце (например, убедиться, что на экране отображается правильный список Foos). Фрагмент/активность/представление - это тот, который наблюдает PagedList<Foo>
из ViewModel.
Ответы
Ответ 1
простой способ добиться этого, - издеваться над PagedList. Эта забава будет "преобразовывать" список в PagedList (в этом случае мы не используем реальную PagedList, а просто издеваемую версию, если вам нужны другие методы PagedList для их добавления, добавьте их в это удовольствие)
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
}
Ответ 2
- Вы не можете использовать List to PagedList.
- Вы не можете создавать PagedList напрямую, только через DataSource. Одним из способов является создание FakeDataSource, возвращающего тестовые данные.
Если это сквозной тест, вы можете просто использовать in-memory db. Добавьте свои тестовые данные перед вызовом. Пример: https://medium.com/exploring-android/android-architecture-components-testing-your-room-dao-classes-e06e1c9a1535
Ответ 3
Преобразование списка в PagedList с фиктивным DataSource.Factory
@saied89 поделился этим решением в этой проблеме googlesamples/android-Architecture-components. Я реализовал фиктивный PagedList в Coinverse Open App для локального модульного тестирования ViewModel с использованием библиотек Kotlin, JUnit 5, MockK и AssertJ.
Для наблюдения LiveData из PagedList я использовал реализацию Хосе Альсеррека из getOrAwaitValue
из примера приложения LiveDataSample в разделе "Примеры компонентов архитектуры Google Google".
Функция расширения asPagedList
реализована в примере теста ContentViewModelTest.kt ниже.
PagedListTestUtil.kt
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(config: PagedList.Config? = null): PagedList<T>? {
val defaultConfig = PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setPageSize(size)
.setMaxSize(size + 2)
.setPrefetchDistance(1)
.build()
return LivePagedListBuilder<Int, T>(
createMockDataSourceFactory(this),
config ?: defaultConfig
).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)
}
}
LiveDataTestUtil.kt
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 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()
[email protected](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
}
ContentViewModelTest.kt
...
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 MutableLiveData<Lce<ContentResult.PagedListResult>>().also { lce ->
lce.value = Lce.Content(
ContentResult.PagedListResult(
pagedList = MutableLiveData<PagedList<Content>>().apply {
this.value = listOf(content).asPagedList(
PagedList.Config.Builder().setEnablePlaceholders(false)
.setPrefetchDistance(24)
.setPageSize(12)
.build())
}, 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)
}
}
InstantExecutorExtension.kt
Это требуется для JUnit 5 при использовании LiveData, чтобы гарантировать, что Observer не находится в главном потоке. Ниже приведена реализация ДжероенаМолса.
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)
}
}