Uygulamanızda Paging kitaplığını kullanmak, sağlam bir test stratejisiyle birlikte yapılmalıdır. PagingSource ve RemoteMediator gibi veri yükleme bileşenlerinin beklendiği gibi çalıştığından emin olmak için bu bileşenleri test etmeniz gerekir. Ayrıca, Paging uygulamanızdaki tüm bileşenlerin beklenmedik yan etkiler olmadan birlikte doğru şekilde çalıştığını doğrulamak için uçtan uca testler de yazmanız gerekir.
Bu kılavuzda, Paging kitaplığını uygulamanızın farklı mimari katmanlarında nasıl test edeceğiniz ve Paging uygulamanızın tamamı için nasıl uçtan uca testler yazacağınız açıklanmaktadır.
Kullanıcı arayüzü katmanı testleri
Compose, Paging verilerini collectAsLazyPagingItems üzerinden bildirime dayalı olarak kullandığından kullanıcı arayüzü katmanı testleriniz tamamen ViewModel'iniz tarafından yayınlanan Flow<PagingData<Value>> öğesine odaklanabilir. Kullanıcı arayüzündeki verilerin beklentilerinizi karşıladığını doğrulamak için testler yazmak istiyorsanız paging-testing bağımlılığını ekleyin. Flow<PagingData<Value>> üzerinde asSnapshot uzantısı bulunur. Lambda alıcısında, kaydırma etkileşimlerinin taklit edilmesine olanak tanıyan API'ler sunar. Kaydırma etkileşimleri taklit edilerek oluşturulan standart bir List<Value> döndürür.
Bu sayede, sayfalandırılan verilerin söz konusu etkileşimler tarafından oluşturulan beklenen öğeleri içerdiğini onaylayabilirsiniz. Bu durum aşağıdaki snippet'te gösterilmektedir:
fun test_items_contain_one_to_ten() = runTest {
// Get the Flow of PagingData from the ViewModel under test
val items: Flow<PagingData<String>> = viewModel.items
val itemsSnapshot: List<String> = items.asSnapshot {
// Scroll to the 50th item in the list. This will also suspend till
// the prefetch requirement is met if there's one.
// It also suspends until all loading is complete.
scrollTo(index = 50)
}
// With the asSnapshot complete, you can now verify that the snapshot
// has the expected values
assertEquals(
expected = (0..50).map(Int::toString),
actual = itemsSnapshot
)
}
Alternatif olarak, aşağıdaki snippet'te gösterildiği gibi belirli bir yüklem karşılanana kadar kaydırabilirsiniz:
fun test_footer_is_visible() = runTest {
// Get the Flow of PagingData from the ViewModel under test
val items: Flow<PagingData<String>> = viewModel.items
val itemsSnapshot: List<String> = items.asSnapshot {
// Scroll till the footer is visible
appendScrollWhile { item: String -> item != "Footer" }
}
Dönüşümleri test etme
Ayrıca PagingData akışına uyguladığınız tüm dönüşümleri kapsayan birim testleri de yazmanız gerekir. asPagingSourceFactory uzantısını kullanın. Bu uzantı aşağıdaki veri türlerinde kullanılabilir:
List<Value>.Flow<List<Value>>.
Hangi uzantıyı kullanacağınız, test etmek istediğiniz şeye bağlıdır. Kullanım:
List<Value>.asPagingSourceFactory(): Veriler üzerindemap()veinsertSeparators()gibi statik dönüşümleri test etmek istiyorsanız.Flow<List<Value>>.asPagingSourceFactory(): Verilerinizdeki güncellemelerin (ör. destekleyen veri kaynağına yazma) sayfalama ardışık düzeninizi nasıl etkilediğini test etmek istiyorsanız.
Bu uzantılardan birini kullanmak için aşağıdaki düzeni uygulayın:
- İhtiyaçlarınıza uygun uzantıyı kullanarak
PagingSourceFactoryoluşturun. Repositoryiçin döndürülenPagingSourceFactorydeğerini sahte olarak kullanın.RepositoryöğesiniViewModelcihazınıza aktarın.
ViewModel, önceki bölümde ele alındığı şekilde test edilebilir.
Aşağıdakileri göz önünde bulundurun ViewModel:
class MyViewModel(
myRepository: myRepository
) {
val items = Pager(
config: PagingConfig,
initialKey = null,
pagingSourceFactory = { myRepository.pagingSource() }
)
.flow
.map { pagingData ->
pagingData.insertSeparators<String, String> { before, _ ->
when {
// Add a dashed String separator if the prior item is a multiple of 10
before.last() == '0' -> "---------"
// Return null to avoid adding a separator between two items.
else -> null
}
}
}
MyViewModel içindeki dönüşümü test etmek için aşağıdaki snippet'te gösterildiği gibi, dönüştürülecek verileri temsil eden statik bir List'ye yetki veren MyRepository sahte örneğini sağlayın:
class FakeMyRepository() : MyRepository {
private val items = (0..100).map(Any::toString)
private val pagingSourceFactory = items.asPagingSourceFactory()
// Expose as a function so a new PagingSource instance is
// created each time it is called by the Pager
fun pagingSource() = pagingSourceFactory()
}
Ardından, aşağıdaki snippet'te gösterildiği gibi ayırıcı mantığı için bir test yazabilirsiniz:
fun test_separators_are_added_every_10_items() = runTest {
// Create your ViewModel
val viewModel = MyViewModel(
myRepository = FakeMyRepository()
)
// Get the Flow of PagingData from the ViewModel with the separator transformations applied
val items: Flow<PagingData<String>> = viewModel.items
val snapshot: List<String> = items.asSnapshot()
// With the asSnapshot complete, you can now verify that the snapshot
// has the expected separators.
}
Veri katmanı testleri
Veri kaynaklarınızdaki verileri uygun şekilde yüklediklerinden emin olmak için veri katmanınızdaki bileşenler için birim testleri yazın. Test edilen bileşenlerin bağımsız olarak doğru şekilde çalıştığını doğrulamak için bağımlılıkların sahte sürümlerini sağlayın. Depo katmanında test etmeniz gereken ana bileşenler PagingSource ve RemoteMediator'dir.
PagingSource testi
PagingSource uygulamanız için birim testleri, PagingSource örneğini ayarlamayı ve TestPager ile bu örnekten veri yüklemeyi içerir.
PagingSource örneğini test için ayarlamak üzere oluşturucuya sahte veriler sağlayın. Bu sayede, testlerinizdeki verileri kontrol edebilirsiniz.
Aşağıdaki örnekte, RedditApi parametresi, sunucu isteklerini ve yanıt sınıflarını tanımlayan bir Retrofit arayüzüdür.
Sahte bir sürüm, arayüzü uygulayabilir, gerekli işlevleri geçersiz kılabilir ve sahte nesnenin testlerde nasıl tepki vermesi gerektiğini yapılandırmak için kolaylık yöntemleri sağlayabilir.
Sahte veriler yerleştirildikten sonra bağımlılıkları ayarlayın ve testteki
PagingSource nesnesini başlatın. Aşağıdaki örnekte, FakeRedditApi nesnesinin bir test gönderileri listesiyle başlatılması ve RedditPagingSource örneğinin test edilmesi gösterilmektedir:
class SubredditPagingSourceTest {
private val mockPosts = listOf(
postFactory.createRedditPost(DEFAULT_SUBREDDIT),
postFactory.createRedditPost(DEFAULT_SUBREDDIT),
postFactory.createRedditPost(DEFAULT_SUBREDDIT)
)
private val fakeApi = FakeRedditApi().apply {
mockPosts.forEach { post -> addPost(post) }
}
@Test
fun loadReturnsPageWhenOnSuccessfulLoadOfItemKeyedData() = runTest {
val pagingSource = RedditPagingSource(
fakeApi,
DEFAULT_SUBREDDIT
)
val pager = TestPager(CONFIG, pagingSource)
val result = pager.refresh() as LoadResult.Page
// Write assertions against the loaded data
assertThat(result.data)
.containsExactlyElementsIn(mockPosts)
.inOrder()
}
}
TestPager simgesiyle aşağıdakileri de yapabilirsiniz:
PagingSourceadresinizden art arda yüklemeleri test edin:
@Test
fun test_consecutive_loads() = runTest {
val page = with(pager) {
refresh()
append()
append()
} as LoadResult.Page
assertThat(page.data)
.containsExactlyElementsIn(testPosts)
.inOrder()
}
PagingSourceuygulamanızdaki hata senaryolarını test edin:
@Test
fun refresh_returnError() {
val pagingSource = RedditPagingSource(
fakeApi,
DEFAULT_SUBREDDIT
)
// Configure your fake to return errors
fakeApi.setReturnsError()
val pager = TestPager(CONFIG, source)
runTest {
source.errorNextLoad = true
val result = pager.refresh()
assertTrue(result is LoadResult.Error)
val page = pager.getLastLoadedPage()
assertThat(page).isNull()
}
}
RemoteMediator testi
RemoteMediator birim testlerinin amacı, load() işlevinin doğru MediatorResult döndürdüğünü doğrulamaktır.
Verilerin veritabanına eklenmesi gibi yan etkilerle ilgili testler, entegrasyon testleri için daha uygundur.
İlk adım, RemoteMediator
uygulamanızın hangi bağımlılıklara ihtiyaç duyduğunu belirlemektir. Aşağıdaki örnekte, Room veritabanı, Retrofit arayüzü ve arama dizesi gerektiren bir RemoteMediator uygulaması gösterilmektedir:
@OptIn(ExperimentalPagingApi::class)
class PageKeyedRemoteMediator(
private val db: RedditDb,
private val redditApi: RedditApi,
private val subredditName: String
) : RemoteMediator<Int, RedditPost>() {
...
}
Retrofit arayüzünü ve arama dizesini PagingSource testleri bölümünde gösterildiği gibi sağlayabilirsiniz. Room veritabanının sahte bir sürümünü sağlamak çok karmaşık bir işlem olduğundan, tam bir sahte sürüm yerine veritabanının bellek içi uygulamasını sağlamak daha kolay olabilir. Room veritabanı oluşturmak için Context nesnesi gerektiğinden bu RemoteMediator testini androidTest dizinine yerleştirmeniz ve test uygulaması bağlamına erişebilmesi için AndroidJUnit4 test çalıştırıcısıyla yürütmeniz gerekir. Enstrümanlı testler hakkında daha fazla bilgi için Enstrümanlı birim testleri oluşturma başlıklı makaleyi inceleyin.
Durumun test işlevleri arasında sızmasını önlemek için temizleme işlevleri tanımlayın. Bu sayede test çalıştırmaları arasında tutarlı sonuçlar elde edebilirsiniz.
@ExperimentalPagingApi
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class PageKeyedRemoteMediatorTest {
private val postFactory = PostFactory()
private val mockPosts = listOf(
postFactory.createRedditPost(SubRedditViewModel.DEFAULT_SUBREDDIT),
postFactory.createRedditPost(SubRedditViewModel.DEFAULT_SUBREDDIT),
postFactory.createRedditPost(SubRedditViewModel.DEFAULT_SUBREDDIT)
)
private val mockApi = mockRedditApi()
private val mockDb = RedditDb.create(
ApplicationProvider.getApplicationContext(),
useInMemory = true
)
@After
fun tearDown() {
mockDb.clearAllTables()
// Clear out failure message to default to the successful response.
mockApi.failureMsg = null
// Clear out posts after each test run.
mockApi.clearPosts()
}
}
Bir sonraki adım, load() işlevini test etmektir. Bu örnekte test edilecek üç durum vardır:
- İlk durumda
mockApigeçerli veriler döndürür.load()işleviMediatorResult.Successdeğerini döndürmeli veendOfPaginationReachedözelliğifalseolmalıdır. - İkinci durum,
mockApibaşarılı bir yanıt döndürdüğünde ancak döndürülen veriler boş olduğunda ortaya çıkar.load()işleviMediatorResult.Successdeğerini döndürmeli veendOfPaginationReachedözelliğitrueolmalıdır. - Üçüncü durum,
mockApiverileri getirirken hata verdiğinde ortaya çıkar.load()işleviMediatorResult.Errordeğerini döndürmelidir.
İlk durumu test etmek için aşağıdaki adımları uygulayın:
- Döndürülecek yayın verileriyle
mockApiöğesini ayarlayın. RemoteMediatornesnesini başlatın.load()işlevini test edin.
@Test
fun refreshLoadReturnsSuccessResultWhenMoreDataIsPresent() = runTest {
// Add mock results for the API to return.
mockPosts.forEach { post -> mockApi.addPost(post) }
val remoteMediator = PageKeyedRemoteMediator(
mockDb,
mockApi,
SubRedditViewModel.DEFAULT_SUBREDDIT
)
val pagingState = PagingState<Int, RedditPost>(
listOf(),
null,
PagingConfig(10),
10
)
val result = remoteMediator.load(LoadType.REFRESH, pagingState)
assertTrue { result is MediatorResult.Success }
assertFalse { (result as MediatorResult.Success).endOfPaginationReached }
}
İkinci testte mockApi boş bir sonuç döndürmelidir. Her test çalıştırmasından sonra mockApi verilerini temizlediğiniz için varsayılan olarak boş bir sonuç döndürür.
@Test
fun refreshLoadSuccessAndEndOfPaginationWhenNoMoreData() = runTest {
// To test endOfPaginationReached, don't set up the mockApi to return post
// data here.
val remoteMediator = PageKeyedRemoteMediator(
mockDb,
mockApi,
SubRedditViewModel.DEFAULT_SUBREDDIT
)
val pagingState = PagingState<Int, RedditPost>(
listOf(),
null,
PagingConfig(10),
10
)
val result = remoteMediator.load(LoadType.REFRESH, pagingState)
assertTrue { result is MediatorResult.Success }
assertTrue { (result as MediatorResult.Success).endOfPaginationReached }
}
Son testte, testin load() fonksiyonunun doğru şekilde MediatorResult.Error döndürdüğünü doğrulayabilmesi için mockApi öğesinin bir istisna oluşturması gerekir.
@Test
fun refreshLoadReturnsErrorResultWhenErrorOccurs() = runTest {
// Set up failure message to throw exception from the mock API.
mockApi.failureMsg = "Throw test failure"
val remoteMediator = PageKeyedRemoteMediator(
mockDb,
mockApi,
SubRedditViewModel.DEFAULT_SUBREDDIT
)
val pagingState = PagingState<Int, RedditPost>(
listOf(),
null,
PagingConfig(10),
10
)
val result = remoteMediator.load(LoadType.REFRESH, pagingState)
assertTrue {result is MediatorResult.Error }
}
Uçtan uca testler
Birim testleri, tek tek Paging bileşenlerinin bağımsız olarak çalıştığına dair güvence verir ancak uçtan uca testler, uygulamanın bir bütün olarak çalıştığına dair daha fazla güven sağlar. Bu testler, veri katmanınızın (PagingSource veya RemoteMediator), ViewModel ve Compose kullanıcı arayüzünün beklenmedik yan etkiler olmadan sorunsuz bir şekilde entegre olduğunu doğrulamanıza yardımcı olur. Testler için yine bazı sahte bağımlılıklar gerekir ancak genellikle uygulama kodunuzun büyük bir bölümünü kapsar.
Bu bölümdeki örnekte, testlerde ağ kullanımını önlemek için sahte bir API bağımlılığı kullanılmaktadır. Sahte API, tutarlı bir test veri grubu döndürecek şekilde yapılandırılır. Bu da tekrarlanabilir testler yapılmasını sağlar. Uçtan uca testlerde, genellikle gerçek ağ API'nizi sahte bir API ile değiştirirsiniz. Ancak testlerinizin doğruluğunu korumak için Paging kitaplığının gerçek getirme ve yerel veritabanı önbelleğe alma işlemlerini (RemoteMediator kullanılıyorsa) yapmasına izin vermeye devam edersiniz.
Kodunuzu, bağımlılıklarınızın sahte sürümlerini kolayca değiştirebileceğiniz şekilde yazın. Aşağıdaki örnekte temel bir hizmet bulucu uygulaması kullanılmakta ve bir Compose ekranının sayfalandırılmış verileri düzgün şekilde kullandığını ve görüntülediğini doğrulamak için sahte bir API ile test ayarlanmaktadır. Daha büyük uygulamalarda, Hilt gibi bir bağımlılık yerleştirme kitaplığı kullanmak daha karmaşık bağımlılık grafiklerini yönetmeye yardımcı olabilir.
Test yapısını ayarladıktan sonraki adım, Pager uygulamasının döndürdüğü verilerin doğru olduğunu doğrulamaktır. Bir test, ekran ilk yüklendiğinde Compose kullanıcı arayüzünün doğru öğelerle doldurulduğunu doğrulamalıdır. Başka bir test ise kullanıcı arayüzünün, kullanıcı etkileşimine bağlı olarak ek verileri doğru şekilde yüklediğini doğrulamalıdır.
Aşağıdaki örnekte, test, kullanıcı arayüzünün beklenen sayfalandırılmış verileri gösterdiğini doğrular.
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertDoesNotExist
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextClearance
import androidx.compose.ui.test.performTextInput
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class RedditScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
private val postFactory = PostFactory()
private val mockApi = MockRedditApi()
@Before
fun setup() {
// Pre-populate the mock API with test data for the default subreddit
mockApi.addPost(postFactory.createRedditPost(subreddit = "androiddev", title = "Jetpack Compose Paging"))
// Swap your real dependency injection module/Service Locator with the mock API
ServiceLocator.swap(
object : DefaultServiceLocator(useInMemoryDb = true) {
override fun getRedditApi(): RedditApi = mockApi
}
)
}
@Test
fun loadsTheDefaultResults() = runTest {
// 1. Set the Compose UI content
composeTestRule.setContent {
MyTheme {
// Assume that this composable uses `collectAsLazyPagingItems()` internally
RedditScreen(initialSubreddit = "androiddev")
}
}
// 2. Wait for the asynchronous Paging loads to complete
composeTestRule.waitUntilExactlyOneExists(
matcher = hasText("Jetpack Compose Paging"),
timeoutMillis = 5000
)
// 3. Assert that the loaded paged items are displayed correctly on screen
composeTestRule.onNodeWithText("Jetpack Compose Paging").assertIsDisplayed()
}
@Test
fun loadsNewDataBasedOnUserInput() = runTest {
// Add data for a different subreddit to the mock API
mockApi.addPost(postFactory.createRedditPost(subreddit = "compose", title = "Compose Testing"))
composeTestRule.setContent {
MyTheme {
RedditScreen(initialSubreddit = "androiddev")
}
}
// Wait for the initial load to finish
composeTestRule.waitUntilExactlyOneExists(hasText("Jetpack Compose Paging"))
// Simulate user entering a new subreddit in a text field and clicking search
composeTestRule.onNodeWithTag("SubredditInput").performTextClearance()
composeTestRule.onNodeWithTag("SubredditInput").performTextInput("compose")
composeTestRule.onNodeWithTag("SearchButton").performClick()
// Wait for the new paged data to load
composeTestRule.waitUntilExactlyOneExists(
matcher = hasText("Compose Testing"),
timeoutMillis = 5000
)
// Assert the old data is gone and the new data is displayed
composeTestRule.onNodeWithText("Jetpack Compose Paging").assertDoesNotExist()
composeTestRule.onNodeWithText("Compose Testing").assertIsDisplayed()
}
}
Flow<PagingData>, verileri eşzamansız olarak yüklediğinden, ilk yüklemeyi getirmesi ve collectAsLazyPagingItems'ye göndermesi için Paging kitaplığına zaman tanımanız gerekir. Bunu yapmak için önceki örnekte gösterildiği gibi composeTestRule.waitUntil veya waitUntilExactlyOneExists simgesini kullanın.
Veriler yüklendikten sonra, öğelerin LazyColumn içinde gerçekten oluşturulduğunu doğrulamak için onNodeWithText kullanarak doğrudan Compose semantik ağacına karşı onaylama yapabilirsiniz.
Ek kaynaklar
İçeriği görüntüleme
Sizin için önerilenler
- Not: JavaScript kapalıyken bağlantı metni gösterilir.
- Ağ ve veritabanından sayfa
- Paging 3'e geçiş
- Sayfalandırılmış verileri yükleme ve görüntüleme