בדיקת הטמעת הדפים

את הטמעה של ספריית ההחלפה באפליקציה צריך להתאים אסטרטגיית בדיקה. צריך לבדוק רכיבים של טעינת נתונים כמו 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.

כדי לבדוק את המקרה הראשון, פועלים לפי השלבים הבאים:

  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));
}

בדיקות מקצה לקצה

בדיקות יחידה נועדו להבטיח שרכיבי ההחלפה הנפרדים פועלים אבל בדיקות מקצה לקצה מספקות ביטחון רב יותר שהיישום פועל כיחידה אחת. בבדיקות האלה עדיין תצטרכו ליצור יחסי תלות מדומה, בדרך כלל הם יכסו את רוב הקוד של האפליקציה.

הדוגמה בקטע הזה משתמשת בתלות מדומה של 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, או על ידי חזרה על הצפיות בשורות הנפרדות אימות שפורמט הנתונים תקין.