پیاده سازی Paging خود را تست کنید

پیاده سازی کتابخانه Paging در برنامه شما باید با یک استراتژی تست قوی همراه باشد. شما باید اجزای بارگذاری داده مانند PagingSource و RemoteMediator را آزمایش کنید تا مطمئن شوید که مطابق انتظار کار می کنند. همچنین باید تست‌های سرتاسری بنویسید تا بررسی کنید که همه اجزای موجود در پیاده‌سازی Paging به درستی بدون عوارض جانبی غیرمنتظره با هم کار می‌کنند.

این راهنما نحوه آزمایش کتابخانه Paging در لایه‌های معماری مختلف برنامه و همچنین نحوه نوشتن تست‌های سرتاسر برای کل پیاده‌سازی Paging را توضیح می‌دهد.

تست لایه رابط کاربری

داده‌های واکشی شده با کتابخانه Paging در رابط کاربری به‌عنوان یک Flow<PagingData<Value>> مصرف می‌شوند. برای نوشتن آزمایش‌هایی برای تأیید داده‌ها در رابط کاربری همانطور که انتظار دارید، وابستگی paging-testing نیز در نظر بگیرید. این شامل پسوند asSnapshot() در یک Flow<PagingData<Value>> است. این 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.
}

تست لایه داده

تست های واحد را برای اجزای لایه داده خود بنویسید تا مطمئن شوید که آنها داده ها را از منابع داده شما به درستی بارگذاری می کنند. نسخه‌های جعلی وابستگی‌ها را ارائه کنید تا تأیید کنید که اجزای تحت آزمایش به‌صورت مجزا به درستی کار می‌کنند. اجزای اصلی که باید در لایه مخزن آزمایش کنید، 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 را نشان می دهد که به یک پایگاه داده اتاق، یک رابط Retrofit و یک رشته جستجو نیاز دارد:

کاتلین

@OptIn(ExperimentalPagingApi::class)
class PageKeyedRemoteMediator(
  private val db: RedditDb,
  private val redditApi: RedditApi,
  private val subredditName: String
) : RemoteMediator<Int, RedditPost>() {
  ...
}

جاوا

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

جاوا

public class PageKeyedRemoteMediator
  extends ListenableFutureRemoteMediator<Integer, RedditPost> {

  @NonNull
  private RedditDb db;
  @NonNull
  private RedditPostDao postDao;
  @NonNull
  private SubredditRemoteKeyDao remoteKeyDao;
  @NonNull
  private RedditApi redditApi;
  @NonNull
  private String subredditName;
  @NonNull
  private Executor bgExecutor;

  public PageKeyedRemoteMediator(
    @NonNull RedditDb db,
    @NonNull RedditApi redditApi,
    @NonNull String subredditName,
    @NonNull Executor bgExecutor
  ) {
    this.db = db;
    this.postDao = db.posts();
    this.remoteKeyDao = db.remoteKeys();
    this.redditApi = redditApi;
    this.subredditName = subredditName;
    this.bgExecutor = bgExecutor;
    ...
  }
}

می توانید رابط Retrofit و رشته جستجو را همانطور که در بخش تست های PagingSource نشان داده شده است ارائه دهید. ارائه یک نسخه ساختگی از پایگاه داده اتاق بسیار مهم است، بنابراین ارائه یک پیاده سازی در حافظه پایگاه داده به جای نسخه کامل ساختگی می تواند آسان تر باشد. از آنجا که ایجاد یک پایگاه داده اتاق به یک شی Context نیاز دارد، باید این تست RemoteMediator را در پوشه androidTest قرار دهید و آن را با اجرای آزمایشی AndroidJUnit4 اجرا کنید تا به زمینه برنامه آزمایشی دسترسی داشته باشد. برای اطلاعات بیشتر در مورد تست های ابزار دقیق، به ساخت تست های واحد ابزار دقیق مراجعه کنید.

برای اطمینان از اینکه حالت بین توابع آزمایشی نشت نمی کند، توابع حذف را تعریف کنید. این امر نتایج یکسانی را بین اجرای آزمایشی تضمین می کند.

کاتلین

@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()
  }
}

جاوا

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

جاوا

@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() را تست کنید.

کاتلین

@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 }
}

جاوا

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

جاوا

@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 پاک می‌کنید، به‌طور پیش‌فرض یک نتیجه خالی برمی‌گرداند.

کاتلین

@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 }
}

جاوا

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

جاوا

@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 برمی گرداند.

کاتلین

@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 }
}

جاوا

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

جاوا

@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 برای جلوگیری از استفاده از شبکه در آزمایشات استفاده می کند. API ساختگی به گونه ای پیکربندی شده است که مجموعه ای ثابت از داده های آزمایشی را برگرداند که منجر به آزمایش های تکرار شونده می شود. بر اساس آنچه که هر وابستگی انجام می دهد، میزان سازگاری خروجی آن و میزان وفاداری که از آزمایشات خود نیاز دارید، تصمیم بگیرید که کدام وابستگی ها را با پیاده سازی های ساختگی جایگزین کنید.

کد خود را به گونه ای بنویسید که به شما امکان می دهد به راحتی نسخه های ساختگی وابستگی های خود را تعویض کنید. مثال زیر از یک پیاده سازی یاب سرویس پایه برای ارائه و تغییر وابستگی ها در صورت نیاز استفاده می کند. در برنامه های بزرگتر، استفاده از کتابخانه تزریق وابستگی مانند Hilt می تواند به مدیریت نمودارهای وابستگی پیچیده تر کمک کند.

کاتلین

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

جاوا

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

جاوا

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 دیگر را برای جستجو وارد می‌کند، به‌روزرسانی می‌کند.

کاتلین

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

جاوا

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

جاوا

@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 وجود دارد، یا با تکرار در نماهای ردیف جداگانه و تأیید اینکه داده‌ها به درستی قالب‌بندی شده‌اند، انجام دهید.

{% کلمه به کلمه %} {% آخر کلمه %} {% کلمه به کلمه %} {% آخر کلمه %}