Menerapkan library Paging di aplikasi Anda harus dipasangkan dengan strategi
pengujian yang kuat. Anda harus menguji komponen pemuatan data, seperti
PagingSource
dan
RemoteMediator
untuk memastikan keduanya berfungsi seperti yang diharapkan. Anda juga harus menulis pengujian menyeluruh untuk
memverifikasi bahwa semua komponen dalam implementasi Paging berfungsi dengan benar
bersama-sama tanpa efek samping yang tidak terduga.
Panduan ini menjelaskan cara menguji library Paging di lapisan arsitektur yang berbeda pada aplikasi Anda, serta cara menulis pengujian menyeluruh untuk seluruh implementasi Paging.
Pengujian lapisan UI
Data yang diambil dengan library Paging digunakan di UI sebagai
Flow<PagingData<Value>>
.
Untuk menulis pengujian guna memverifikasi data di UI seperti yang Anda harapkan, sertakan
dependensi paging-testing
.
Dependensi ini berisi ekstensi asSnapshot()
pada Flow<PagingData<Value>>
. Hal ini
memungkinkan Anda menyatakan bahwa data yang di-page berisi elemen yang diharapkan dan menggunakan
List<Value>
standar. Hal ini diilustrasikan dalam cuplikan berikut:
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(
coroutineScope = this
) {
// Each operation inside the lambda waits for the data to settle before continuing
scrollTo(index = 50)
// While you can’t view the items within the asSnapshot call,
// you can continuously scroll in a direction while some condition is true
// i.e., in this case until you hit the first header.
appendScrollWhile { item: String -> item != "Header 1" }
}
// With the asSnapshot complete, you can now verify that the snapshot
// has the expected values
assertEquals(
expected = (0..50).map(Int::toString),
actual = itemsSnapshot
)
}
Menguji transformasi
Anda juga harus menulis pengujian unit yang mencakup transformasi apa pun yang diterapkan ke
aliran PagingData
. Untuk menulis pengujian ini:
- Gunakan ekstensi
asPagingSourceFactory
diFlow<List<Value>>
. - Gunakan
PagingSourceFactory
yang ditampilkan dalam palsu untukRepository
. - Teruskan
Repository
tersebut keViewModel
Anda.
ViewModel
kemudian dapat diuji seperti yang dibahas di bagian sebelumnya.
Pertimbangkan ViewModel
berikut:
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
}
}
}
Untuk menguji transformasi di MyViewModel
, sediakan instance
MyRepository
palsu yang didelegasikan ke Flow
yang mewakili data yang akan diubah
seperti yang ditunjukkan dalam cuplikan berikut:
class FakeMyRepository(
testScope: CoroutineScope
): MyRepository {
private val itemsFlow = flowOf((0..100).map(Any::toString))
private val pagingSourceFactory = itemsFlow.asPagingSourceFactory(
coroutineScope = testScope
)
val pagingSource = pagingSourceFactory()
}
Kemudian, Anda dapat menulis pengujian untuk logika pemisah seperti dalam cuplikan berikut:
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(coroutineScope = this) {}
// With the asSnapshot complete, you can now verify that the snapshot
// has the expected separators
}
Pengujian lapisan data
Tulis pengujian unit untuk komponen dalam lapisan data Anda guna memastikan bahwa unit
memuat data dari sumber data Anda dengan tepat. Berikan versi
tiruan dependensi untuk memverifikasi bahwa komponen yang sedang diuji berfungsi dengan benar secara
terpisah. Komponen utama yang perlu Anda uji di lapisan repositori adalah
PagingSource
dan RemoteMediator
. Contoh di bagian berikut ini akan didasarkan pada
Contoh Paging dengan
Jaringan.
Pengujian PagingSource
Pengujian unit untuk class PagingSource
melibatkan penyiapan instance PagingSource
dan memverifikasi bahwa
fungsi load()
menampilkan
data yang di-page dengan benar berdasarkan
argumen
LoadParams
. Berikan data tiruan ke konstruktor PagingSource
, sehingga Anda memiliki
kontrol atas data dalam pengujian Anda. Anda dapat meneruskan nilai default untuk jenis data dasar
tetapi Anda harus meneruskan versi tiruan untuk objek lain,
seperti database atau implementasi API. Tindakan ini memberi Anda kontrol penuh atas
output sumber data palsu saat PagingSource
yang sedang diuji berinteraksi
dengan sumber data tersebut.
Parameter konstruktor untuk implementasi PagingSource
spesifik
Anda menentukan jenis data pengujian yang harus dilewati. Dalam contoh berikut,
implementasi PagingSource
memerlukan objek RedditApi
serta
String
untuk nama subreddit. Anda dapat meneruskan nilai default untuk parameter String
,
tetapi untuk parameter RedditApi
, Anda harus membuat implementasi
tiruan untuk pengujian.
Kotlin
class ItemKeyedSubredditPagingSource( private val redditApi: RedditApi, private val subredditName: String ) : PagingSource<String, RedditPost>() { ... }
Java
public class ItemKeyedSubredditPagingSource extends RxPagingSource<String, RedditPost> { @NonNull private RedditApi redditApi; @NonNull private String subredditName; ItemKeyedSubredditPagingSource( @NotNull RedditApi redditApi, @NotNull String subredditName ) { this.redditApi = redditApi; this.subredditName = subredditName; ... } }
Java
public class ItemKeyedSubredditPagingSource extends ListenableFuturePagingSource<String, RedditPost> { @NonNull private RedditApi redditApi; @NonNull private String subredditName; @NonNull private Executor bgExecutor; public ItemKeyedSubredditPagingSource( @NotNull RedditApi redditApi, @NotNull String subredditName, @NonNull Executor bgExecutor ) { this.redditApi = redditApi; this.subredditName = subredditName; this.bgExecutor = bgExecutor; ... } }
Dalam contoh ini, parameter RedditApi
adalah
antarmuka Retrofit yang menentukan permintaan server serta class respons. Versi tiruan dapat
menerapkan antarmuka, mengganti fungsi yang diperlukan, dan menyediakan
metode yang mudah untuk mengonfigurasi reaksi objek tiruan dalam pengujian.
Setelah objek tiruan tersedia, siapkan dependensi dan inisialisasi objek
PagingSource
dalam pengujian. Contoh berikut menunjukkan
inisialisasi objek MockRedditApi
dengan daftar postingan pengujian:
Kotlin
@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) } } @Test fun loadReturnsPageWhenOnSuccessfulLoadOfItemKeyedData() = runTest { val pagingSource = ItemKeyedSubredditPagingSource(mockApi, DEFAULT_SUBREDDIT) } }
Java
public class SubredditPagingSourceTest { static PostFactory postFactory = new PostFactory(); static List<RedditPost> mockPosts = new ArrayList<>(); static MockRedditApi mockApi = new MockRedditApi(); static { for (int i=0; i<3; i++) { mockPosts.add(post); mockApi.addPost(post); } } @After tearDown() { // run. mockApi.setFailureMsg(null); } @Test loadReturnsPageWhenOnSuccessfulLoadOfItemKeyedData() { ItemKeyedSubredditPagingSource(mockApi, DEFAULT_SUBREDDIT); } } RedditPost post=postFactory.createRedditPost(DEFAULT_SUBREDDIT); public void Clear the failure message after each test public void throws InterruptedException ItemKeyedSubredditPagingSource pagingSource=new>
Java
public class SubredditPagingSourceTest { static PostFactory postFactory = new PostFactory(); static List<RedditPost> mockPosts = new ArrayList<>(); static MockRedditApi mockApi = new MockRedditApi(); static { for (int i=0; i<3; i++) { mockPosts.add(post); mockApi.addPost(post); } } @After tearDown() { // run. mockApi.setFailureMsg(null); } @Test loadReturnsPageWhenOnSuccessfulLoadOfItemKeyedData() ExecutionException, { ItemKeyedSubredditPagingSource( mockApi, DEFAULT_SUBREDDIT, CurrentThreadExecutor() ); } } RedditPost post=postFactory.createRedditPost(DEFAULT_SUBREDDIT); public void Clear the failure message after each test public void throws InterruptedException ItemKeyedSubredditPagingSource pagingSource=new new>
Langkah berikutnya adalah menguji objek PagingSource
. Metode load()
adalah
fokus utama pengujian. Contoh berikut menunjukkan pernyataan
yang memverifikasi bahwa metode load()
menampilkan data yang benar, kunci sebelumnya, dan
kunci berikutnya untuk parameter LoadParams
tertentu:
Kotlin
@Test // Since load is a suspend function, runTest is used to ensure that it // runs on the test thread. fun loadReturnsPageWhenOnSuccessfulLoadOfItemKeyedData() = runTest { 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 ) ), ) }
Java
@Test public void loadReturnsPageWhenOnSuccessfulLoadOfItemKeyedData() throws InterruptedException { ItemKeyedSubredditPagingSource pagingSource = new ItemKeyedSubredditPagingSource(mockApi, DEFAULT_SUBREDDIT); LoadParams.Refresh<String> refreshRequest = new LoadParams.Refresh<>(null, 2, false); pagingSource.loadSingle(refreshRequest) .test() .await() .assertValueCount(1) .assertValue(new LoadResult.Page<>( mockPosts.subList(0, 2), mockPosts.get(0).getName(), mockPosts.get(1).getName() )); }
Java
@Test public void loadReturnsPageWhenOnSuccessfulLoadOfItemKeyedData() throws ExecutionException, InterruptedException { ItemKeyedSubredditPagingSource pagingSource = new ItemKeyedSubredditPagingSource( mockApi, DEFAULT_SUBREDDIT, new CurrentThreadExecutor() ); PagingSource.LoadParams.Refresh<String> refreshRequest = new PagingSource.LoadParams.Refresh<>(null, 2, false); PagingSource.LoadResult<String, RedditPost> result = pagingSource.loadFuture(refreshRequest).get(); PagingSource.LoadResult<String, RedditPost> expected = new PagingSource.LoadResult.Page<>( mockPosts.subList(0, 2), mockPosts.get(0).getName(), mockPosts.get(1).getName() ); assertThat(result, equalTo(expected)); }
Contoh sebelumnya menunjukkan pengujian PagingSource
yang menggunakan paging
dengan kunci item. Jika sumber data yang Anda gunakan menggunakan kunci halaman, maka pengujian
PagingSource
akan berbeda. Perbedaan utamanya adalah kunci sebelumnya dan berikutnya
yang diharapkan dari metode load()
.
Kotlin
@Test fun loadReturnsPageWhenOnSuccessfulLoadOfPageKeyedData() = runTest { val pagingSource = PageKeyedSubredditPagingSource(mockApi, DEFAULT_SUBREDDIT) assertEquals( expected = Page( data = listOf(mockPosts[0], mockPosts[1]), prevKey = null, nextKey = mockPosts[1].id ), actual = pagingSource.load( Refresh( key = null, loadSize = 2, placeholdersEnabled = false ) ), ) }
Java
@Test public void loadReturnsPageWhenOnSuccessfulLoadOfPageKeyedData() throws InterruptedException { PageKeyedSubredditPagingSource pagingSource = new PageKeyedSubredditPagingSource(mockApi, DEFAULT_SUBREDDIT); LoadParams.Refresh<String> refreshRequest = new LoadParams.Refresh<>(null, 2, false); pagingSource.loadSingle(refreshRequest) .test() .await() .assertValueCount(1) .assertValue(new LoadResult.Page<>( mockPosts.subList(0, 2), null, mockPosts.get(1).getId() )); }
Java
@Test public void loadReturnsPageWhenOnSuccessfulLoadOfPageKeyedData() throws InterruptedException, ExecutionException { PageKeyedSubredditPagingSource pagingSource = new PageKeyedSubredditPagingSource( mockApi, DEFAULT_SUBREDDIT, new CurrentThreadExecutor() ); PagingSource.LoadParams.Refresh<String> refreshRequest = new PagingSource.LoadParams.Refresh<>(null, 2, false); PagingSource.LoadResult<String, RedditPost> result = pagingSource.loadFuture(refreshRequest).get(); PagingSource.LoadResult<String, RedditPost> expected = new PagingSource.LoadResult.Page<>( mockPosts.subList(0, 2), null, mockPosts.get(1).getId() ); assertThat(result, equalTo(expected)); }
Pengujian RemoteMediator
Sasaran pengujian unit RemoteMediator
adalah untuk memverifikasi bahwa fungsi load()
menampilkan MediatorResult
yang benar.
Pengujian efek samping, seperti data yang dimasukkan ke dalam database,
lebih cocok untuk pengujian integrasi.
Langkah pertama adalah menentukan dependensi yang diperlukan
implementasi RemoteMediator
Anda. Contoh berikut menunjukkan implementasi RemoteMediator
yang memerlukan database Room, antarmuka Retrofit, dan string
penelusuran:
Kotlin
@OptIn(ExperimentalPagingApi::class) class PageKeyedRemoteMediator( private val db: RedditDb, private val redditApi: RedditApi, private val subredditName: String ) : RemoteMediator<Int, RedditPost>() { ... }
Java
public class PageKeyedRemoteMediator extends RxRemoteMediator<Integer, RedditPost> { @NonNull private RedditDb db; @NonNull private RedditPostDao postDao; @NonNull private SubredditRemoteKeyDao remoteKeyDao; @NonNull private RedditApi redditApi; @NonNull private String subredditName; public PageKeyedRemoteMediator( @NonNull RedditDb db, @NonNull RedditApi redditApi, @NonNull String subredditName ) { this.db = db; this.postDao = db.posts(); this.remoteKeyDao = db.remoteKeys(); this.redditApi = redditApi; this.subredditName = subredditName; ... } }
Java
public class PageKeyedRemoteMediator extends ListenableFutureRemoteMediator<Integer, RedditPost> { @NonNull private RedditDb db; @NonNull private RedditPostDao postDao; @NonNull private SubredditRemoteKeyDao remoteKeyDao; @NonNull private RedditApi redditApi; @NonNull private String subredditName; @NonNull private Executor bgExecutor; public PageKeyedRemoteMediator( @NonNull RedditDb db, @NonNull RedditApi redditApi, @NonNull String subredditName, @NonNull Executor bgExecutor ) { this.db = db; this.postDao = db.posts(); this.remoteKeyDao = db.remoteKeys(); this.redditApi = redditApi; this.subredditName = subredditName; this.bgExecutor = bgExecutor; ... } }
Anda dapat memberikan antarmuka Retrofit dan string penelusuran seperti yang ditunjukkan di
bagian pengujian PagingSource. Menyediakan versi tiruan
database Room sangat diperlukan, sehingga dapat lebih mudah untuk menyediakan
implementasi dalam memori
dari database daripada versi tiruan lengkap. Karena membuat database Room
memerlukan objek Context
, Anda harus
menempatkan pengujian RemoteMediator
ini di direktori androidTest
dan menjalankannya
dengan runner pengujian AndroidJUnit4 sehingga memiliki akses ke konteks aplikasi
pengujian. Untuk informasi selengkapnya tentang pengujian berinstrumen, lihat Membuat pengujian unit berinstrumen.
Tentukan fungsi penghapusan untuk memastikan bahwa status tidak bocor di antara fungsi pengujian. Hal ini untuk memastikan hasil yang konsisten di antara pengujian.
Kotlin
@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() } }
Java
@RunWith(AndroidJUnit4.class) public class PageKeyedRemoteMediatorTest { static PostFactory postFactory = new PostFactory(); static List<RedditPost> mockPosts = new ArrayList<>(); static MockRedditApi mockApi = new MockRedditApi(); private RedditDb mockDb = RedditDb.Companion.create( ApplicationProvider.getApplicationContext(), true ); static { for (int i=0; i<3; i++) { RedditPost post = postFactory.createRedditPost(DEFAULT_SUBREDDIT); mockPosts.add(post); } } @After public void tearDown() { mockDb.clearAllTables(); // Clear the failure message after each test run. mockApi.setFailureMsg(null); // Clear out posts after each test run. mockApi.clearPosts(); } }
Java
@RunWith(AndroidJUnit4.class) public class PageKeyedRemoteMediatorTest { static PostFactory postFactory = new PostFactory(); static List<RedditPost> mockPosts = new ArrayList<>(); static MockRedditApi mockApi = new MockRedditApi(); private RedditDb mockDb = RedditDb.Companion.create( ApplicationProvider.getApplicationContext(), true ); static { for (int i=0; i<3; i++) { RedditPost post = postFactory.createRedditPost(DEFAULT_SUBREDDIT); mockPosts.add(post); } } @After public void tearDown() { mockDb.clearAllTables(); // Clear the failure message after each test run. mockApi.setFailureMsg(null); // Clear out posts after each test run. mockApi.clearPosts(); } }
Langkah berikutnya adalah menguji fungsi load()
. Dalam contoh ini, ada tiga
kasus yang harus diuji:
- Kasus pertama adalah saat
mockApi
menampilkan data yang valid. Fungsiload()
akan menampilkanMediatorResult.Success
, dan propertiendOfPaginationReached
harusfalse
. - Kasus kedua adalah saat
mockApi
mengembalikan respons yang berhasil, tetapi data yang ditampilkan kosong. Fungsiload()
harus menampilkanMediatorResult.Success
, dan propertiendOfPaginationReached
harustrue
. - Kasus ketiga adalah saat
mockApi
memunculkan pengecualian sewaktu mengambil data. Fungsiload()
harus menampilkanMediatorResult.Error
.
Ikuti langkah-langkah berikut untuk menguji kasus pertama:
- Siapkan
mockApi
dengan data postingan yang akan ditampilkan. - Inisialisasi objek
RemoteMediator
. - Uji fungsi
load()
Kotlin
@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 } }
Java
@Test public void refreshLoadReturnsSuccessResultWhenMoreDataIsPresent() throws InterruptedException { // Add mock results for the API to return. for (RedditPost post: mockPosts) { mockApi.addPost(post); } PageKeyedRemoteMediator remoteMediator = new PageKeyedRemoteMediator( mockDb, mockApi, SubRedditViewModel.DEFAULT_SUBREDDIT ); PagingState<Integer, RedditPost> pagingState = new PagingState<>( new ArrayList(), null, new PagingConfig(10), 10 ); remoteMediator.loadSingle(LoadType.REFRESH, pagingState) .test() .await() .assertValueCount(1) .assertValue(value -> value instanceof RemoteMediator.MediatorResult.Success && ((RemoteMediator.MediatorResult.Success) value).endOfPaginationReached() == false); }
Java
@Test public void refreshLoadReturnsSuccessResultWhenMoreDataIsPresent() throws InterruptedException, ExecutionException { // Add mock results for the API to return. for (RedditPost post: mockPosts) { mockApi.addPost(post); } PageKeyedRemoteMediator remoteMediator = new PageKeyedRemoteMediator( mockDb, mockApi, SubRedditViewModel.DEFAULT_SUBREDDIT, new CurrentThreadExecutor() ); PagingState<Integer, RedditPost> pagingState = new PagingState<>( new ArrayList(), null, new PagingConfig(10), 10 ); RemoteMediator.MediatorResult result = remoteMediator.loadFuture(LoadType.REFRESH, pagingState).get(); assertThat(result, instanceOf(RemoteMediator.MediatorResult.Success.class)); assertFalse(((RemoteMediator.MediatorResult.Success) result).endOfPaginationReached()); }
Pengujian kedua memerlukan mockApi
untuk menampilkan hasil kosong. Karena Anda
menghapus data dari mockApi
setelah setiap pengujian dijalankan, data akan menampilkan
hasil kosong secara default.
Kotlin
@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 } }
Java
@Test public void refreshLoadSuccessAndEndOfPaginationWhenNoMoreData() throws InterruptedException() { // To test endOfPaginationReached, don't set up the mockApi to return post // data here. PageKeyedRemoteMediator remoteMediator = new PageKeyedRemoteMediator( mockDb, mockApi, SubRedditViewModel.DEFAULT_SUBREDDIT ); PagingState<Integer, RedditPost> pagingState = new PagingState<>( new ArrayList(), null, new PagingConfig(10), 10 ); remoteMediator.loadSingle(LoadType.REFRESH, pagingState) .test() .await() .assertValueCount(1) .assertValue(value -> value instanceof RemoteMediator.MediatorResult.Success && ((RemoteMediator.MediatorResult.Success) value).endOfPaginationReached() == true); }
Java
@Test public void refreshLoadSuccessAndEndOfPaginationWhenNoMoreData() throws InterruptedException, ExecutionException { // To test endOfPaginationReached, don't set up the mockApi to return post // data here. PageKeyedRemoteMediator remoteMediator = new PageKeyedRemoteMediator( mockDb, mockApi, SubRedditViewModel.DEFAULT_SUBREDDIT, new CurrentThreadExecutor() ); PagingState<Integer, RedditPost> pagingState = new PagingState<>( new ArrayList(), null, new PagingConfig(10), 10 ); RemoteMediator.MediatorResult result = remoteMediator.loadFuture(LoadType.REFRESH, pagingState).get(); assertThat(result, instanceOf(RemoteMediator.MediatorResult.Success.class)); assertTrue(((RemoteMediator.MediatorResult.Success) result).endOfPaginationReached()); }
Pengujian terakhir memerlukan mockApi
untuk menampilkan pengecualian sehingga pengujian dapat
memverifikasi bahwa fungsi load()
menampilkan MediatorResult.Error
dengan benar.
Kotlin
@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 } }
Java
@Test public void refreshLoadReturnsErrorResultWhenErrorOccurs() throws InterruptedException { // Set up failure message to throw exception from the mock API. mockApi.setFailureMsg("Throw test failure"); PageKeyedRemoteMediator remoteMediator = new PageKeyedRemoteMediator( mockDb, mockApi, SubRedditViewModel.DEFAULT_SUBREDDIT ); PagingState<Integer, RedditPost> pagingState = new PagingState<>( new ArrayList(), null, new PagingConfig(10), 10 ); remoteMediator.loadSingle(LoadType.REFRESH, pagingState) .test() .await() .assertValueCount(1) .assertValue(value -> value instanceof RemoteMediator.MediatorResult.Error); }
Java
@Test public void refreshLoadReturnsErrorResultWhenErrorOccurs() throws InterruptedException, ExecutionException { // Set up failure message to throw exception from the mock API. mockApi.setFailureMsg("Throw test failure"); PageKeyedRemoteMediator remoteMediator = new PageKeyedRemoteMediator( mockDb, mockApi, SubRedditViewModel.DEFAULT_SUBREDDIT, new CurrentThreadExecutor() ); PagingState<Integer, RedditPost> pagingState = new PagingState<>( new ArrayList(), null, new PagingConfig(10), 10 ); RemoteMediator.MediatorResult result = remoteMediator.loadFuture(LoadType.REFRESH, pagingState).get(); assertThat(result, instanceOf(RemoteMediator.MediatorResult.Error.class)); }
Pengujian menyeluruh
Pengujian unit memberikan jaminan bahwa setiap komponen Paging berfungsi secara terpisah, tetapi pengujian menyeluruh memberikan keyakinan lebih bahwa aplikasi berfungsi secara keseluruhan. Pengujian ini masih memerlukan beberapa dependensi tiruan, tetapi umumnya mencakup sebagian besar kode aplikasi Anda.
Contoh di bagian ini menggunakan dependensi tiruan API untuk menghindari penggunaan jaringan dalam pengujian. API tiruan dikonfigurasi untuk mengembalikan kumpulan data pengujian yang konsisten, sehingga menghasilkan pengujian berulang. Tentukan dependensi yang akan ditukar dengan implementasi tiruan berdasarkan hal yang dilakukan setiap dependensi, seberapa konsisten outputnya, dan seberapa banyak fidelitas yang Anda butuhkan dari pengujian.
Tulis kode Anda dengan cara yang memudahkan Anda menukar versi tiruan dependensi Anda. Contoh berikut menggunakan implementasi pencari lokasi layanan dasar untuk menyediakan dan mengubah dependensi sesuai kebutuhan. Pada aplikasi yang lebih besar, penggunaan library injeksi dependensi seperti Hilt dapat membantu mengelola grafik dependensi yang lebih kompleks.
Kotlin
class RedditActivityTest { companion object { private const val TEST_SUBREDDIT = "test" } private val postFactory = PostFactory() private val mockApi = MockRedditApi().apply { addPost(postFactory.createRedditPost(DEFAULT_SUBREDDIT)) addPost(postFactory.createRedditPost(TEST_SUBREDDIT)) addPost(postFactory.createRedditPost(TEST_SUBREDDIT)) } @Before fun init() { val app = ApplicationProvider.getApplicationContext<Application>() // Use a controlled service locator with a mock API. ServiceLocator.swap( object : DefaultServiceLocator(app = app, useInMemoryDb = true) { override fun getRedditApi(): RedditApi = mockApi } ) } }
Java
public class RedditActivityTest { public static final String TEST_SUBREDDIT = "test"; private static PostFactory postFactory = new PostFactory(); private static MockRedditApi mockApi = new MockRedditApi(); static { mockApi.addPost(postFactory.createRedditPost(DEFAULT_SUBREDDIT)); mockApi.addPost(postFactory.createRedditPost(TEST_SUBREDDIT)); mockApi.addPost(postFactory.createRedditPost(TEST_SUBREDDIT)); } @Before public void setup() { Application app = ApplicationProvider.getApplicationContext(); // Use a controlled service locator with a mock API. ServiceLocator.Companion.swap( new DefaultServiceLocator(app, true) { @NotNull @Override public RedditApi getRedditApi() { return mockApi; } } ); } }
Java
public class RedditActivityTest { public static final String TEST_SUBREDDIT = "test"; private static PostFactory postFactory = new PostFactory(); private static MockRedditApi mockApi = new MockRedditApi(); static { mockApi.addPost(postFactory.createRedditPost(DEFAULT_SUBREDDIT)); mockApi.addPost(postFactory.createRedditPost(TEST_SUBREDDIT)); mockApi.addPost(postFactory.createRedditPost(TEST_SUBREDDIT)); } @Before public void setup() { Application app = ApplicationProvider.getApplicationContext(); // Use a controlled service locator with a mock API. ServiceLocator.Companion.swap( new DefaultServiceLocator(app, true) { @NotNull @Override public RedditApi getRedditApi() { return mockApi; } } ); } }
Setelah Anda menyiapkan struktur pengujian, langkah berikutnya adalah memverifikasi bahwa data
yang ditampilkan oleh implementasi Pager
sudah benar. Satu pengujian harus memastikan bahwa objek Pager
memuat data default saat halaman pertama dimuat, dan pengujian lainnya
harus memverifikasi bahwa objek Pager
memuat data tambahan dengan benar berdasarkan
input pengguna. Pada contoh berikut, pengujian memverifikasi bahwa objek Pager
memperbarui RecyclerView.Adapter
dengan jumlah item
yang benar yang dikembalikan dari API saat pengguna memasukkan subreddit yang berbeda ke penelusuran.
Kotlin
@Test fun loadsTheDefaultResults() { ActivityScenario.launch(RedditActivity::class.java) onView(withId(R.id.list)).check { view, noViewFoundException -> if (noViewFoundException != null) { throw noViewFoundException } val recyclerView = view as RecyclerView assertEquals(1, recyclerView.adapter?.itemCount) } } @Test // Verify that the default data is swapped out when the user searches for a // different subreddit. fun loadsTheTestResultsWhenSearchingForSubreddit() { ActivityScenario.launch(RedditActivity::class.java ) onView(withId(R.id.list)).check { view, noViewFoundException -> if (noViewFoundException != null) { throw noViewFoundException } val recyclerView = view as RecyclerView // Verify that it loads the default data first. assertEquals(1, recyclerView.adapter?.itemCount) } // Search for test subreddit instead of default to trigger new data load. onView(withId(R.id.input)).perform( replaceText(TEST_SUBREDDIT), pressKey(KeyEvent.KEYCODE_ENTER) ) onView(withId(R.id.list)).check { view, noViewFoundException -> if (noViewFoundException != null) { throw noViewFoundException } val recyclerView = view as RecyclerView assertEquals(2, recyclerView.adapter?.itemCount) } }
Java
@Test public void loadsTheDefaultResults() { ActivityScenario.launch(RedditActivity.class); onView(withId(R.id.list)).check((view, noViewFoundException) -> { if (noViewFoundException != null) { throw noViewFoundException; } RecyclerView recyclerView = (RecyclerView) view; assertEquals(1, recyclerView.getAdapter().getItemCount()); }); } @Test // Verify that the default data is swapped out when the user searches for a // different subreddit. public void loadsTheTestResultsWhenSearchingForSubreddit() { ActivityScenario.launch(RedditActivity.class); onView(withId(R.id.list)).check((view, noViewFoundException) -> { if (noViewFoundException != null) { throw noViewFoundException; } RecyclerView recyclerView = (RecyclerView) view; // Verify that it loads the default data first. assertEquals(1, recyclerView.getAdapter().getItemCount()); }); // Search for test subreddit instead of default to trigger new data load. onView(withId(R.id.input)).perform( replaceText(TEST_SUBREDDIT), pressKey(KeyEvent.KEYCODE_ENTER) ); onView(withId(R.id.list)).check((view, noViewFoundException) -> { if (noViewFoundException != null) { throw noViewFoundException; } RecyclerView recyclerView = (RecyclerView) view; assertEquals(2, recyclerView.getAdapter().getItemCount()); }); }
Java
@Test public void loadsTheDefaultResults() { ActivityScenario.launch(RedditActivity.class); onView(withId(R.id.list)).check((view, noViewFoundException) -> { if (noViewFoundException != null) { throw noViewFoundException; } RecyclerView recyclerView = (RecyclerView) view; assertEquals(1, recyclerView.getAdapter().getItemCount()); }); } @Test // Verify that the default data is swapped out when the user searches for a // different subreddit. public void loadsTheTestResultsWhenSearchingForSubreddit() { ActivityScenario.launch(RedditActivity.class); onView(withId(R.id.list)).check((view, noViewFoundException) -> { if (noViewFoundException != null) { throw noViewFoundException; } RecyclerView recyclerView = (RecyclerView) view; // Verify that it loads the default data first. assertEquals(1, recyclerView.getAdapter().getItemCount()); }); // Search for test subreddit instead of default to trigger new data load. onView(withId(R.id.input)).perform( replaceText(TEST_SUBREDDIT), pressKey(KeyEvent.KEYCODE_ENTER) ); onView(withId(R.id.list)).check((view, noViewFoundException) -> { if (noViewFoundException != null) { throw noViewFoundException; } RecyclerView recyclerView = (RecyclerView) view; assertEquals(2, recyclerView.getAdapter().getItemCount()); }); }
Pengujian berinstrumen harus memverifikasi bahwa data ditampilkan dengan benar di UI. Lakukan
ini dengan memverifikasi bahwa jumlah item yang benar ada di
RecyclerView.Adapter
, atau dengan melakukan iterasi melalui tampilan baris masing-masing dan
memverifikasi bahwa data diformat dengan benar.