การนำไลบรารีการแบ่งหน้ามาใช้ในแอปควรจับคู่กับ
กลยุทธ์การทดสอบ คุณควรทดสอบคอมโพเนนต์การโหลดข้อมูล เช่น
PagingSource
และ
RemoteMediator
เพื่อให้แน่ใจว่าทำงานได้ตามที่คาดไว้ และคุณควรเขียนการทดสอบตั้งแต่ต้นจนจบใน
ตรวจสอบว่าคอมโพเนนต์ทั้งหมดในการติดตั้งใช้งานการแบ่งหน้าทำงานอย่างถูกต้อง
โดยไม่เกิดผลข้างเคียงที่ไม่คาดคิด
คู่มือนี้จะอธิบายวิธีทดสอบไลบรารีการแบ่งหน้าใน เลเยอร์ของสถาปัตยกรรมของแอปและวิธีเขียน การทดสอบแบบ end-to-end สำหรับการติดตั้งใช้งาน Paging ทั้งหมด
การทดสอบเลเยอร์ UI
ข้อมูลที่ดึงด้วยไลบรารีการสร้างหน้าจะใช้ใน UI เนื่องจาก
Flow<PagingData<Value>>
หากต้องการเขียนการทดสอบเพื่อยืนยันว่าข้อมูลใน UI เป็นไปตามที่คาดไว้ ให้ใส่
ทรัพยากร Dependency paging-testing
มีส่วนขยาย asSnapshot()
ใน Flow<PagingData<Value>>
ทั้งนี้
มี API ในตัวรับสัญญาณ lambda ที่ให้คุณเลียนแบบการเลื่อนได้
การโต้ตอบ แสดง List<Value>
มาตรฐานที่สร้างโดยการเลื่อน
ที่จำลองการโต้ตอบขึ้น ซึ่งทำให้คุณสามารถยืนยันข้อมูล
หน้าเว็บผ่าน ประกอบด้วยองค์ประกอบที่คาดไว้ซึ่งสร้างขึ้นจากการโต้ตอบเหล่านั้น
มีภาพตัวอย่างดังต่อไปนี้
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
)
}
หรือคุณสามารถเลื่อนจนกว่าจะเป็นไปตามเงื่อนไขที่กำหนดตามที่แสดงใน ตัวอย่างด้านล่าง
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" }
}
การทดสอบการเปลี่ยนรูปแบบ
คุณควรเขียนการทดสอบ 1 หน่วยที่ครอบคลุมการเปลี่ยนรูปแบบที่คุณใช้ด้วย
สตรีม PagingData
ใช้ asPagingSourceFactory
ส่วนขยาย ส่วนขยายนี้ใช้งานได้กับข้อมูลประเภทต่อไปนี้
List<Value>
Flow<List<Value>>
ตัวเลือกในการใช้ส่วนขยายจะขึ้นอยู่กับสิ่งที่คุณพยายามทดสอบ ใช้:
List<Value>.asPagingSourceFactory()
: หากต้องการทดสอบแบบคงที่ เช่นmap()
และinsertSeparators()
ในข้อมูลFlow<List<Value>>.asPagingSourceFactory()
: หากต้องการทดสอบวิธีการอัปเดต ลงในข้อมูล เช่น การเขียนไปยังแหล่งข้อมูลสนับสนุน จะส่งผลต่อการแบ่งหน้า ไปป์ไลน์
หากต้องการใช้ส่วนขยายเหล่านี้ ให้ใช้รูปแบบต่อไปนี้
- สร้าง
PagingSourceFactory
โดยใช้ส่วนขยายที่เหมาะสมสำหรับ ความต้องการ - ใช้
PagingSourceFactory
ที่ส่งคืนใน ปลอม สำหรับRepository
- ส่ง
Repository
ไปยังViewModel
จากนั้นคุณสามารถทดสอบ ViewModel
ได้เหมือนอย่างที่อธิบายไว้ในส่วนก่อนหน้า
ลองพิจารณา 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
ให้ระบุอินสแตนซ์ปลอมของ
MyRepository
ที่มอบสิทธิ์ให้กับ List
แบบคงที่ซึ่งเป็นตัวแทนของข้อมูลที่จะ
เปลี่ยนรูปแบบตามที่แสดงในตัวอย่างต่อไปนี้
class FakeMyRepository(): MyRepository {
private val items = (0..100).map(Any::toString)
private val pagingSourceFactory = items.asPagingSourceFactory()
val pagingSource = pagingSourceFactory()
}
จากนั้นคุณสามารถเขียนการทดสอบสำหรับตรรกะตัวคั่นตามตัวอย่างในตัวอย่างต่อไปนี้
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.
}
การทดสอบชั้นข้อมูล
เขียนการทดสอบ 1 หน่วยสำหรับคอมโพเนนต์ในชั้นข้อมูลเพื่อให้แน่ใจว่า
โหลดข้อมูลจากแหล่งข้อมูลของคุณอย่างเหมาะสม โปรดระบุ
ปลอมของ
ของทรัพยากร Dependency เพื่อยืนยันว่าคอมโพเนนต์ภายใต้ฟังก์ชันทดสอบอย่างถูกต้องใน
การแยก คอมโพเนนต์หลักที่คุณต้องทดสอบในเลเยอร์ที่เก็บคือ
PagingSource
และ RemoteMediator
ตัวอย่างในส่วนต่อไปขึ้นอยู่กับ
การจัดหน้าด้วยเครือข่าย
ตัวอย่าง
การทดสอบ PagingSource
การทดสอบ 1 หน่วยสำหรับการติดตั้งใช้งาน PagingSource
เกี่ยวข้องกับการตั้งค่า
PagingSource
อินสแตนซ์และการโหลดข้อมูลจากอินสแตนซ์ด้วย TestPager
หากต้องการตั้งค่าอินสแตนซ์ PagingSource
สำหรับการทดสอบ ให้ระบุข้อมูลปลอมลงในส่วน
เครื่องมือสร้างขึ้นมา ซึ่งจะช่วยให้คุณควบคุมข้อมูลในการทดสอบได้
ในตัวอย่างต่อไปนี้ RedditApi
เป็นพารามิเตอร์ Retrofit
ซึ่งกำหนดคำขอของเซิร์ฟเวอร์และคลาสการตอบสนอง
เวอร์ชันปลอมสามารถใช้อินเทอร์เฟซ ลบล้างฟังก์ชันที่จำเป็น
และมอบวิธีการที่สะดวกในการกำหนดค่าว่า ออบเจ็กต์ปลอมควรตอบสนองอย่างไร
ในการทดสอบ
หลังจากมีของปลอมแล้ว ให้ตั้งค่า Dependency และเริ่ม
PagingSource
ออบเจ็กต์ในการทดสอบ ตัวอย่างต่อไปนี้แสดง
การเริ่มต้นออบเจ็กต์ FakeRedditApi
ด้วยรายการโพสต์ทดสอบ และการทดสอบ
อินสแตนซ์ RedditPagingSource
:
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
ยังให้คุณทําสิ่งต่อไปนี้ได้ด้วย
- ทดสอบการโหลดติดต่อกันจาก
PagingSource
ของคุณ:
@Test
fun test_consecutive_loads() = runTest {
val page = with(pager) {
refresh()
append()
append()
} as LoadResult.Page
assertThat(page.data)
.containsExactlyElementsIn(testPosts)
.inOrder()
}
- สถานการณ์ข้อผิดพลาดการทดสอบใน
PagingSource
@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
เป้าหมายของRemoteMediator
การทดสอบ 1 หน่วยคือการตรวจสอบว่าload()
จะแสดงผลค่าที่ถูกต้อง
MediatorResult
การทดสอบผลข้างเคียง เช่น ข้อมูลที่ถูกแทรกในฐานข้อมูล มีดังนี้
เหมาะอย่างยิ่งสำหรับการทดสอบการผสานรวม
ขั้นตอนแรกคือการกำหนดทรัพยากร Dependency สำหรับ RemoteMediator
ความต้องการในการติดตั้งใช้งาน ตัวอย่างต่อไปนี้แสดงRemoteMediator
ซึ่งจำเป็นต้องมีฐานข้อมูลห้อง อินเทอร์เฟซ Retrofit และการค้นหา
สตริง:
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; ... } }
คุณสามารถระบุอินเทอร์เฟซ Retrofit และสตริงการค้นหาดังที่แสดงใน
ส่วนการทดสอบ PagingSource การจัดเตรียมเวอร์ชันจำลอง
ของฐานข้อมูลห้องเกี่ยวข้องกันอย่างมาก จึงสามารถให้
การใช้งานในหน่วยความจำของ
ฐานข้อมูลแทนที่จะเป็นเวอร์ชันจำลองเต็มรูปแบบ เพราะการสร้างฐานข้อมูลห้อง
ต้องมีออบเจ็กต์ Context
วางการทดสอบ RemoteMediator
นี้ในไดเรกทอรี androidTest
และดำเนินการ
ด้วยตัวดำเนินการทดสอบ AndroidJUnit4 เพื่อให้แอปเข้าถึงแอปพลิเคชันทดสอบได้
บริบท ดูข้อมูลเพิ่มเติมเกี่ยวกับการทดสอบแบบมีเครื่องควบคุมได้ที่สร้างแบบมีเครื่องควบคุม
การทดสอบ 1 หน่วย
กำหนดฟังก์ชันการฉีกขาดเพื่อให้มั่นใจว่าสถานะจะไม่รั่วไหลระหว่างการทดสอบ วิธีนี้ช่วยให้มั่นใจได้ว่าผลการทดสอบจะสอดคล้องกัน
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(); } }
ขั้นตอนถัดไปคือการทดสอบฟังก์ชัน load()
ในตัวอย่างนี้มี 3 ประเภท
กรณีที่ต้องทดสอบ:
- กรณีแรกคือเมื่อ
mockApi
แสดงผลข้อมูลที่ถูกต้อง ฟังก์ชันload()
ควรแสดงผลMediatorResult.Success
และendOfPaginationReached
ควรเป็นfalse
- กรณีที่ 2 คือเมื่อ
mockApi
แสดงการตอบกลับที่สำเร็จ แต่ ข้อมูลที่ส่งคืนว่างเปล่า ฟังก์ชันload()
ควรแสดงผลMediatorResult.Success
และพร็อพเพอร์ตี้endOfPaginationReached
ควรเป็นtrue
- กรณีที่ 3 คือเมื่อ
mockApi
ส่งข้อยกเว้นเมื่อดึงข้อมูล ฟังก์ชันload()
ควรแสดงผลMediatorResult.Error
ทำตามขั้นตอนต่อไปนี้เพื่อทดสอบกรณีแรก
- ตั้งค่า
mockApi
ที่มีข้อมูลโพสต์ที่จะแสดงผล - เริ่มต้นออบเจ็กต์
RemoteMediator
- ทดสอบฟังก์ชัน
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()); }
การทดสอบครั้งที่ 2 กำหนดให้ mockApi
แสดงผลลัพธ์ที่ว่างเปล่า เนื่องจากคุณ
ล้างข้อมูลจาก mockApi
หลังจากทำการทดสอบแต่ละครั้ง ระบบจะแสดงผลค่าที่ว่างเปล่า
ผลลัพธ์โดยค่าเริ่มต้น
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()); }
การทดสอบครั้งสุดท้ายกำหนดให้ mockApi
ต้องทิ้งข้อยกเว้นเพื่อให้การทดสอบสามารถ
ตรวจสอบว่าฟังก์ชัน load()
แสดงผล MediatorResult.Error
อย่างถูกต้อง
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)); }
การทดสอบแบบเอนด์ทูเอนด์
การทดสอบ 1 หน่วยช่วยให้มั่นใจว่าคอมโพเนนต์ของการแบ่งหน้าแต่ละคอมโพเนนต์ทำงานได้ การแยกอย่างเป็นอิสระ แต่การทดสอบจากต้นทางถึงปลายทาง จะให้ความมั่นใจมากขึ้นว่าแอปพลิเคชัน จะทำงานโดยรวม การทดสอบเหล่านี้ยังต้องมีการอ้างอิงตัวอย่างบ้าง แต่ มักครอบคลุมโค้ดส่วนใหญ่ของแอป
ตัวอย่างในส่วนนี้ใช้การพึ่งพา API จำลองเพื่อหลีกเลี่ยงการใช้ เครือข่ายในการทดสอบ มีการกำหนดค่า API จำลองให้แสดงผลชุดการทดสอบที่สอดคล้องกัน ทำให้เกิดการทดสอบที่ทำซ้ำได้ เลือกว่าจะเปลี่ยนไปใช้ทรัพยากร Dependency ใด จำลองการติดตั้งใช้งานโดยพิจารณาการทำงานของทรัพยากร Dependency แต่ละรายการ ความสม่ำเสมอ และความแม่นยำที่คุณต้องการจากการทดสอบ
เขียนโค้ดในลักษณะที่ช่วยให้คุณสลับเวอร์ชันจำลอง ทรัพยากร Dependency ตัวอย่างต่อไปนี้ใช้ เครื่องระบุตำแหน่งบริการ พื้นฐาน เพื่อมอบและ เปลี่ยนทรัพยากร Dependency ที่จำเป็น ในแอปขนาดใหญ่ ให้ใช้การแทรกทรัพยากร Dependency ไลบรารีอย่าง Hilt สามารถช่วยจัดการ กราฟการขึ้นต่อกันที่ซับซ้อนขึ้น
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; } } ); } }
หลังจากตั้งค่าโครงสร้างการทดสอบแล้ว ขั้นตอนถัดไปคือการตรวจสอบว่าข้อมูล
ที่แสดงผลโดยการใช้งาน Pager
นั้นถูกต้อง การทดสอบ 1 รายการควรตรวจสอบให้แน่ใจว่า
ออบเจ็กต์ Pager
จะโหลดข้อมูลเริ่มต้นเมื่อโหลดหน้าเว็บครั้งแรก และโหลดอีกรายการหนึ่ง
การทดสอบควรยืนยันว่าออบเจ็กต์ Pager
โหลดข้อมูลเพิ่มเติมได้อย่างถูกต้องตาม
ในข้อมูลจากผู้ใช้ ในตัวอย่างต่อไปนี้ การทดสอบจะยืนยันว่า Pager
ออบเจ็กต์อัปเดต RecyclerView.Adapter
ด้วยจำนวนรายการที่ถูกต้อง
ที่แสดงผลจาก API เมื่อผู้ใช้ป้อน Subreddit อื่นเพื่อค้นหา
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()); }); }
การทดสอบที่มีเครื่องวัดควรตรวจสอบว่าข้อมูลแสดงอย่างถูกต้องใน UI ควรทำ
ด้วยการยืนยันว่ามีจำนวนรายการที่ถูกต้องอยู่ในฟิลด์
RecyclerView.Adapter
หรือทำซ้ำผ่านมุมมองแต่ละแถวและ
ตรวจสอบว่าข้อมูลอยู่ในรูปแบบที่ถูกต้องหรือไม่
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- หน้าเว็บจากเครือข่ายและฐานข้อมูล
- ย้ายไปหน้า 3
- โหลดและแสดงข้อมูลแบบแบ่งหน้า