التحقق من تنفيذ عملية "نقل الصفحات"

يجب اقتران تنفيذ مكتبة تسجيل الصفحات في تطبيقك باستراتيجية اختبار قوية. يجب اختبار مكوّنات تحميل البيانات، مثل 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.

اتبع الخطوات التالية لاختبار الحالة الأولى:

  1. يمكنك إعداد mockApi باستخدام بيانات المشاركة لعرضها.
  2. إعداد الكائن RemoteMediator
  3. اختبار الدالة 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، أو من خلال التكرار في طرق عرض الصفوف الفردية والتحقق من تنسيق البيانات بشكل صحيح.