يجب اقتران تنفيذ مكتبة تسجيل الصفحات في تطبيقك باستراتيجية
اختبار قوية. يجب اختبار مكوّنات تحميل البيانات، مثل PagingSource
وRemoteMediator
، للتأكّد من أنّها تعمل على النحو المتوقّع. عليك أيضًا كتابة اختبارات شاملة للتأكّد من أنّ جميع مكونات تنفيذ وضع الصفحات في موقعك الإلكتروني تعمل بشكل صحيح معًا بدون أي آثار جانبية غير متوقعة.
يشرح هذا الدليل كيفية اختبار مكتبة تسجيل الصفحات في طبقات البنية المختلفة لتطبيقك فضلاً عن كيفية كتابة اختبارات شاملة لتنفيذ عملية "تقسيم الصفحات" بالكامل.
اختبارات طبقة واجهة المستخدم
يتم استهلاك البيانات التي يتم استرجاعها من خلال مكتبة تسجيل الصفحات في واجهة المستخدم باعتبارها Flow<PagingData<Value>>
.
لكتابة اختبارات للتحقق من صحة البيانات في واجهة المستخدم كما تتوقّع، يجب تضمين
تبعية paging-testing
.
وتتضمّن الإضافة "asSnapshot()
" في Flow<PagingData<Value>>
. ويوفّر هذا البرنامج واجهات برمجة تطبيقات في مستلِم 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" }
}
اختبار التحويلات
عليك أيضًا كتابة اختبارات وحدات تتضمّن أي عمليات تحويل تطبّقها على
مصدر بيانات 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.
}
اختبارات طبقة البيانات
اكتب اختبارات وحدات للمكونات في طبقة بياناتك للتأكد من أنها تحمِّل البيانات من مصادر بياناتك بشكل مناسب. قدّم إصدارات مزيفة من
التبعيات للتحقق من أن المكونات تحت الاختبار تعمل بشكل صحيح في العزل. المكوّن الرئيسي الذي تحتاج إلى اختباره في طبقة المستودع هما PagingSource
وRemoteMediator
. تستند الأمثلة الواردة في الأقسام التالية إلى
نموذج تنسيق الصفحات باستخدام شبكة.
اختبارات PagingSource
تتضمّن اختبارات الوحدات لتنفيذ PagingSource
إعداد مثيل PagingSource
وتحميل البيانات منه باستخدام TestPager
.
لإعداد مثيل PagingSource
للاختبار، يجب تقديم بيانات مزيفة للدالة الإنشائية. ويتيح لك ذلك التحكم في البيانات في اختباراتك.
في المثال التالي، المعلَمة 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
تتطلّب قاعدة بيانات غرفة وواجهة تعديل وسلسلة بحث:
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; ... } }
يمكنك تقديم واجهة التعديل وسلسلة البحث كما هو موضّح في قسم اختبارات PagingSource. يُعد تقديم نسخة وهمية من قاعدة بيانات الغرفة أمرًا مهمًا للغاية، لذا قد يكون من الأسهل توفير تنفيذ في الذاكرة لقاعدة البيانات بدلاً من نسخة وهمية كاملة. بما أنّ إنشاء قاعدة بيانات غرفة يتطلب كائن Context
، يجب وضع اختبار RemoteMediator
هذا في دليل androidTest
وتنفيذه باستخدام برنامج تشغيل الاختبار AndroidJUnit4 حتى يتمكن من الوصول إلى سياق تطبيق الاختبار. لمزيد من المعلومات عن الاختبارات المُعدّة، راجع إنشاء اختبارات الوحدات
المدفوعة.
حدد دوال التقسيم للتأكد من عدم تسرب الحالة بين دوال الاختبار. وهذا يضمن الحصول على نتائج متسقة بين عمليات إجراء الاختبارات.
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)); }
الاختبارات الشاملة
تقدم اختبارات الوحدات ضمانًا بأن مكونات تسجيل الصفحات تعمل في حالة منعزلة، إلا أن الاختبارات الشاملة تثق بقدر أكبر على أن التطبيق يعمل ككل. ستظل هذه الاختبارات بحاجة إلى بعض التبعيات الوهمية، ولكنها بشكل عام ستغطي معظم رمز تطبيقك.
يستخدم المثال في هذا القسم تبعية واجهة برمجة التطبيقات وهمية لتجنب استخدام الشبكة في الاختبارات. تتم تهيئة واجهة برمجة التطبيقات التجريبية لإرجاع مجموعة متسقة من بيانات الاختبار، مما يؤدي إلى إنشاء اختبارات قابلة للتكرار. حدد التبعيات المطلوب استبدالها بعمليات تنفيذ وهمية بناءً على ما تفعله كل تبعية، ومدى اتساق مخرجاتها، ومقدار الدقة التي تحتاجها من اختباراتك.
اكتب التعليمة البرمجية بطريقة تتيح لك التبديل بسهولة في إصدارات وهمية من تبعياتك. يستخدم المثال التالي تنفيذ محدد مواقع الخدمة الأساسي لتوفير التبعيات وتغييرها حسب الحاجة. في التطبيقات الأكبر حجمًا، يمكن أن يساعد استخدام مكتبة حقن التبعية مثل 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
بالعدد الصحيح من العناصر التي تم إرجاعها من واجهة برمجة التطبيقات عندما يُدخل المستخدم قيمة فرعية مختلفة على Reddit للبحث.
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
- تحميل البيانات المقسّمة على صفحات وعرضها