את הטמעה של ספריית ההחלפה באפליקציה צריך להתאים
אסטרטגיית בדיקה. צריך לבדוק רכיבים של טעינת נתונים כמו
PagingSource
ו-
RemoteMediator
כדי להבטיח שהן יפעלו כצפוי. צריך גם לכתוב בדיקות מקצה לקצה
לוודא שכל הרכיבים בהטמעת ההחלפה פועלים כמו שצריך
יחד ללא תופעות לוואי בלתי צפויות.
במדריך הזה מוסבר איך בודקים את ספריית ההחלפה שכבות הארכיטקטורה של האפליקציה, וגם איך לכתוב בדיקות מקצה לקצה עבור כל הטמעת הקידוד.
בדיקות שכבות של ממשק משתמש
נתונים שמאוחזרים באמצעות ספריית הדפים נצרכים בממשק המשתמש באופן הבא:
Flow<PagingData<Value>>
.
כדי לכתוב בדיקות לאימות הנתונים בממשק המשתמש כפי שציפיתם, צריך לכלול את
תלות ב-paging-testing
.
היא מכילה את התוסף asSnapshot()
ב-Flow<PagingData<Value>>
. הוא
במקלט ה-lambda שלו ממשקי API מאפשרים גלילה מדומה
האינטראקציות. היא מחזירה 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" }
}
בדיקת טרנספורמציות
צריך גם לכתוב בדיקות יחידה שמכסות את הטרנספורמציות שעליהן מחילים
הזרם 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.
}
בדיקות של שכבות נתונים
כתבו בדיקות יחידה (unit testing) של הרכיבים בשכבת הנתונים כדי לוודא
לטעון את הנתונים ממקורות הנתונים בצורה נכונה. יש לספק
גרסאות מזויפות של
של הרכיבים בבדיקה כדי לוודא שהרכיבים בבדיקה,
של טרנספורמר. הרכיבים העיקריים שצריך לבדוק בשכבת המאגר הם
PagingSource
וגם RemoteMediator
. הדוגמאות בקטעים הבאים מבוססות על
החלפה עם רשת
דוגמה.
בדיקות PagingSource
בדיקות היחידה של ההטמעה של PagingSource
כוללות הגדרה של
מכונה PagingSource
וטוענים ממנה נתונים באמצעות TestPager
.
כדי להגדיר את המכונה של PagingSource
לבדיקה, צריך לספק נתונים מזויפים
constructor. כך אפשר לשלוט בנתונים שבבדיקות.
בדוגמה הבאה, הפרמטר RedditApi
הפרמטר הוא Retrofit
ממשק שמגדיר את בקשות השרת ואת סוגי התגובות.
גרסה מזויפת יכולה להטמיע את הממשק, לבטל פונקציות נדרשות
ומספקים שיטות נוחות כדי להגדיר איך האובייקט המזויף צריך להגיב
בבדיקות.
לאחר שהזיופים נמצאים, מגדירים את יחסי התלות ומאתחלים
אובייקט אחד (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
היא לאמת שמאפיין load()
מחזירה את הפונקציה
MediatorResult
.
בדיקות של תופעות לוואי, כגון הוספת נתונים למסד הנתונים,
מתאים יותר לבדיקות שילוב.
השלב הראשון הוא לקבוע אילו יחסי תלות של RemoteMediator
את צורכי ההטמעה. הדוגמה הבאה ממחישה איך RemoteMediator
שמחייב מסד נתונים Room, ממשק Retrofit
string:
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 checks. שליחת גרסת הדמיה
של מסד הנתונים Room מאוד מעורב, כך שקל יותר לספק
הטמעה בזיכרון של
את מסד הנתונים במקום גרסת הדמיה מלאה. כי יצירת מסד נתונים מסוג Room
מחייב אובייקט Context
, צריך
להציב את הבדיקה הזו RemoteMediator
בספרייה androidTest
ולהפעיל אותה
באמצעות מריץ הבדיקה AndroidJUnit4 כך שתהיה לו גישה לאפליקציית בדיקה
הקשר מסוים. מידע נוסף על בדיקות מכשירים זמין במאמר Build instrumented
בדיקות יחידה (unit testing).
הגדרת פונקציות הסרה כדי לוודא שהמצב לא דלף בין הבדיקות למשימות ספציפיות. כך אפשר להבטיח תוצאות עקביות בין הפעלות הבדיקה.
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()
. בדוגמה הזאת יש שלוש
מקרים לבדיקה:
- המקרה הראשון הוא כאשר
mockApi
מחזיר נתונים תקינים. הפונקציהload()
הפונקציה אמורה להחזיר אתMediatorResult.Success
, והפונקציהendOfPaginationReached
צריך להיותfalse
. - המקרה השני הוא כאשר
mockApi
מחזיר תגובה מוצלחת, אבל ריקים, ריקים. הפונקציהload()
צריכה להחזירMediatorResult.Success
, והמאפייןendOfPaginationReached
צריך להיותtrue
- המקרה השלישי הוא כאשר
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()); }
בבדיקה השנייה נדרשת הפונקציה 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)); }
בדיקות מקצה לקצה
בדיקות יחידה נועדו להבטיח שרכיבי ההחלפה הנפרדים פועלים אבל בדיקות מקצה לקצה מספקות ביטחון רב יותר שהיישום פועל כיחידה אחת. בבדיקות האלה עדיין תצטרכו ליצור יחסי תלות מדומה, בדרך כלל הם יכסו את רוב הקוד של האפליקציה.
הדוגמה בקטע הזה משתמשת בתלות מדומה של API כדי להימנע משימוש ברשת בבדיקות. ה-mock API מוגדר כך שיחזיר קבוצה עקבית של בדיקות התוצאה היא בדיקות שחוזרות על עצמן. החליטו באילו יחסי תלות להחליף להדמיה של הטמעות שמבוססות על מה שכל תלות עושה, עד כמה העקביות שלה הפלט שלכם, ומה מידת הדיוק שתצטרכו מהבדיקות שלכם.
כתבו את הקוד באופן שיאפשר לכם להחליף בקלות בגרסאות הדומות של של יחסי התלות. הדוגמה הבאה משתמשת במאתר שירות בסיסי ההטמעה כדי לספק לשנות את יחסי התלות לפי הצורך. באפליקציות גדולות יותר, באמצעות החדרת תלות ספרייה כמו 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
הם נכונים. בדיקה אחת אמורה להבטיח
האובייקט 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()); }); }
בדיקות אינסטרומנטליות צריכות לוודא שהנתונים מוצגים כראוי בממשק המשתמש. מומלץ
על ידי אימות מספר הפריטים הנכון,
RecyclerView.Adapter
, או על ידי חזרה על הצפיות בשורות הנפרדות
אימות שפורמט הנתונים תקין.
מומלץ עבורך
- הערה: טקסט הקישור מוצג כאשר JavaScript מושבת
- דף מהרשת וממסד הנתונים
- העברה לדף 3
- טעינה והצגה של נתונים בדפים