Kiểm thử việc triển khai Phân trang (Paging)

Việc triển khai thư viện Paging trong ứng dụng của bạn nên được kết hợp với một chiến lược kiểm thử mạnh mẽ. Bạn nên kiểm thử các thành phần tải dữ liệu, như PagingSourceRemoteMediator để đảm bảo các thành phần này hoạt động như mong đợi. Bạn cũng nên viết mã kiểm thử toàn diện để xác minh rằng tất cả các thành phần trong quá trình triển khai Paging (Phân trang) hoạt động đúng cách cùng nhau mà không có ảnh hưởng bất lợi ngoài dự kiến nào.

Hướng dẫn này giải thích cách kiểm thử thư viện Paging trong các lớp cấu trúc khác nhau của ứng dụng cũng như cách viết mã kiểm thử toàn diện cho toàn bộ quá trình triển khai Paging.

Kiểm thử lớp giao diện người dùng

Dữ liệu đã tìm nạp bằng thư viện Paging được sử dụng trong giao diện người dùng dưới dạng Flow<PagingData<Value>>. Để viết mã kiểm thử nhằm xác minh dữ liệu trong giao diện người dùng đúng như bạn mong đợi, hãy thêm phần phụ thuộc paging-testing. Phần phụ thuộc này chứa phương thức mở rộng asSnapshot() trên Flow<PagingData<Value>>. Đồng thời cũng cung cấp các API trong trình thu nhận lambda của chính nó, cho phép mô phỏng hoạt động tương tác cuộn. Phần phụ thuộc này còn trả về một List<Value> tiêu chuẩn do các hoạt động mô phỏng tương tác cuộn tạo ra, cho phép bạn xác nhận xem các dữ liệu đã phân trang có chứa phần tử dự kiến được tạo bởi các hoạt động tương tác đó hay không. Điều này được minh hoạ trong đoạn mã sau:

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

Ngoài ra, bạn có thể cuộn cho đến khi một vị từ (predicate) nhất định được đáp ứng như trong đoạn mã dưới đây:


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

Kiểm thử các phép biến đổi

Bạn cũng nên viết mã kiểm thử đơn vị bao gồm mọi phép biến đổi mà bạn áp dụng cho luồng PagingData. Sử dụng hàm mở rộng asPagingSourceFactory Hàm này dùng được với các kiểu dữ liệu sau:

  • List<Value>.
  • Flow<List<Value>>.

Việc lựa chọn sử dụng hàm mở rộng nào phụ thuộc vào nội dung bạn đang cố gắng kiểm thử. Sử dụng:

  • List<Value>.asPagingSourceFactory(): Nếu bạn muốn kiểm thử các phép biến đổi tĩnh như map()insertSeparators() trên dữ liệu.
  • Flow<List<Value>>.asPagingSourceFactory(): Nếu bạn muốn kiểm thử mức độ ảnh hưởng của các nội dung cập nhật về dữ liệu (ví dụ: ghi vào nguồn dữ liệu hỗ trợ) đến quy trình phân trang.

Để sử dụng một trong các hàm mở rộng này, hãy thực hiện theo mẫu sau:

  • Tạo PagingSourceFactory bằng cách sử dụng hàm mở rộng thích hợp theo nhu cầu của bạn.
  • Sử dụng PagingSourceFactory được trả về dưới dạng một giao diện giả cho Repository.
  • Truyền Repository đó đến ViewModel của bạn.

Sau đó, bạn có thể kiểm thử ViewModel như đã đề cập ở phần trước. Cân nhắc ViewModel sau đây:

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

Để kiểm thử phép biến đổi trong MyViewModel, hãy cung cấp một thực thể giả của MyRepository và uỷ quyền cho một List tĩnh. Mã này giúp trình bày dữ liệu cần được biến đổi như minh hoạ trong đoạn mã sau:

class FakeMyRepository(): MyRepository {
    private val items = (0..100).map(Any::toString)

    private val pagingSourceFactory = items.asPagingSourceFactory()

    val pagingSource = pagingSourceFactory()
}

Sau đó, bạn có thể viết mã kiểm thử cho logic của dòng phân cách như trong đoạn mã sau:

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

Kiểm thử lớp dữ liệu

Viết mã kiểm thử đơn vị cho các thành phần trong lớp dữ liệu của bạn để đảm bảo chúng tải dữ liệu từ các nguồn dữ liệu của bạn một cách thích hợp. Cung cấp phiên bản giả của các phần phụ thuộc để bảo đảm các thành phần kiểm thử hoạt động đúng cách khi tách biệt. Thành phần chính bạn cần kiểm thử trong lớp lưu trữ là PagingSourceRemoteMediator. Ví dụ trong các phần cần theo dõi dựa trên bài viết Phân trang bằng mẫu Network.

Kiểm thử PagingSource

Các chương trình kiểm thử đơn vị để triển khai PagingSource liên quan đến việc thiết lập thực thể PagingSource và tải dữ liệu từ thực thể đó bằng TestPager.

Để thiết lập thực thể PagingSource cho việc kiểm thử, hãy cung cấp dữ liệu giả cho hàm khởi tạo. Thao tác này cho phép bạn kiểm soát dữ liệu trong các bài kiểm thử của mình. Trong ví dụ sau, tham số RedditApi là giao diện Retrofit xác định các yêu cầu máy chủ và các lớp (class) phản hồi. Phiên bản giả có thể triển khai giao diện, ghi đè mọi hàm bắt buộc và cung cấp các phương thức thuận tiện để định cấu hình cách đối tượng giả sẽ phản hồi trong các lần kiểm thử.

Sau khi các lớp giả được đưa ra, hãy thiết lập các phần phụ thuộc và khởi chạy đối tượng PagingSource trong chương trình kiểm thử. Ví dụ sau minh hoạ việc khởi chạy đối tượng FakeRedditApi bằng một danh sách các bài đăng kiểm thử và kiểm thử thực thể 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 cũng cho phép bạn làm những việc sau:

  • Kiểm thử tải liên tục từ PagingSource:
    @Test
    fun test_consecutive_loads() = runTest {

      val page = with(pager) {
        refresh()
        append()
        append()
      } as LoadResult.Page

      assertThat(page.data)
      .containsExactlyElementsIn(testPosts)
      .inOrder()
    }
  • Kiểm thử các trường hợp lỗi trong 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()
        }
    }

Kiểm thử RemoteMediator

Mục tiêu của kiểm thử đơn vị RemoteMediator là nhằm xác minh rằng hàm load() trả về đúng MediatorResult. Các loại hình kiểm thử về tác dụng phụ, chẳng hạn như dữ liệu bị chèn vào cơ sở dữ liệu, là lựa chọn phù hợp hơn cho kiểm thử tích hợp.

Bước đầu tiên là xác định những phần phụ thuộc cần cho quá trình triển khai RemoteMediator của bạn. Ví dụ sau minh hoạ quá trình triển khai RemoteMediator. Quá trình này yêu cầu cơ sở dữ liệu Room, giao diện Retrofit và cụm từ tìm kiếm:

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

Bạn có thể cung cấp giao diện Retrofit và cụm từ tìm kiếm như minh hoạ trong mục kiểm thử PagingSource. Việc cung cấp một phiên bản mô phỏng của cơ sở dữ liệu Room rất quan trọng. Vì vậy, việc cung cấp cách triển khai trong bộ nhớ của cơ sở dữ liệu có thể dễ dàng hơn là một phiên bản mô phỏng đầy đủ. Vì việc tạo cơ sở dữ liệu Room yêu cầu phải có đối tượng Context, bạn phải đặt loại hình kiểm thử RemoteMediator này vào thư mục androidTest và thực thi bằng đối tượng trình chạy kiểm thử AndroidJUnit4 để có quyền truy cập vào ngữ cảnh của ứng dụng kiểm thử. Để biết thêm thông tin về loại hình kiểm thử được đo lường, hãy xem phần Tạo loại hình kiểm thử đơn vị được đo lường.

Xác định các hàm phân giải để đảm bảo rằng trạng thái đó không bị rò rỉ giữa các hàm kiểm thử. Điều này đảm bảo kết quả nhất quán giữa các lần chạy kiểm thử.

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

Bước tiếp theo là kiểm thử hàm load(). Trong ví dụ này, có ba trường hợp cần kiểm thử:

  • Trường hợp đầu tiên là mockApi trả về dữ liệu hợp lệ. Hàm load() cần trả về MediatorResult.Success và thuộc tính endOfPaginationReached phải là false.
  • Trường hợp thứ hai là mockApi trả về một phản hồi thành công, nhưng dữ liệu trả về bị trống. Hàm load() cần trả về MediatorResult.Success và thuộc tính endOfPaginationReached phải là true.
  • Trường hợp thứ ba là mockApi gửi một ngoại lệ khi tìm nạp dữ liệu. Hàm load() cần trả về MediatorResult.Error.

Hãy thực hiện theo các bước sau để kiểm thử trường hợp đầu tiên:

  1. Thiết lập mockApi với dữ liệu bài đăng cần trả về.
  2. Khởi động đối tượng RemoteMediator.
  3. Kiểm tra hàm 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());
}

Trường hợp kiểm thử thứ hai yêu cầu mockApi trả về kết quả trống. Vì bạn xoá dữ liệu khỏi mockApi sau mỗi lần chạy kiểm thử nên kết quả trả về sẽ trống theo mặc định.

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

Trường hợp kiểm thử cuối cùng yêu cầu mockApi gửi một ngoại lệ để quá trình kiểm thử có thể xác minh rằng hàm load() trả về MediatorResult.Error chính xác.

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

Kiểm thử toàn diện

Loại hình kiểm thử đơn vị nhằm đảm bảo rằng các thành phần Paging riêng lẻ hoạt động tách biệt, nhưng loại hình kiểm thử toàn diện đảm bảo rằng ứng dụng hoạt động như một thể thống nhất. Các loại hình kiểm thử này vẫn cần một số phần phụ thuộc mô phỏng, nhưng thường sẽ có hầu hết mã trong ứng dụng của bạn.

Ví dụ trong mục này sử dụng phần phụ thuộc API mô phỏng để tránh sử dụng mạng trong quá trình kiểm thử. API mô phỏng được định cấu hình để trả về một tập dữ liệu kiểm thử nhất quán, giúp các lần kiểm thử có thể lặp lại. Bạn có thể quyết định cần hoán đổi phần phụ thuộc nào trong các lần triển khai mô phỏng dựa trên chức năng của mỗi phần phụ thuộc, mức độ nhất quán của kết quả và mức độ trung thực mà bạn cần từ những lần kiểm thử.

Viết mã của bạn theo cách dễ dàng cho phép hoán đổi trong các phiên bản mô phỏng của các phần phụ thuộc. Ví dụ sau đây sử dụng một quy trình triển khai bộ định vị dịch vụ cơ bản để cung cấp và thay đổi các phần phụ thuộc (nếu cần). Trong các ứng dụng lớn, việc sử dụng thư viện chèn phần phụ thuộc như Hilt có thể giúp quản lý các biểu đồ phần phụ thuộc phức tạp hơn.

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

Sau khi bạn thiết lập cấu trúc kiểm thử, bước tiếp theo là xác minh rằng dữ liệu mà hoạt động triển khai Pager trả về là chính xác. Một quy trình kiểm thử phải đảm bảo rằng đối tượng Pager tải dữ liệu mặc định khi trang tải lần đầu tiên và một quy trình kiểm thử khác cần xác minh rằng đối tượng Pager tải đúng dữ liệu bổ sung dựa trên hoạt động đầu vào của người dùng. Trong ví dụ sau, quy trình kiểm thử xác minh rằng đối tượng Pager cập nhật RecyclerView.Adapter với số lượng mục chính xác được trả về từ API khi người dùng nhập một chuyên mục khác để tìm kiếm.

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

Những lần kiểm thử được đo lường cần xác minh rằng dữ liệu hiển thị chính xác trong giao diện người dùng. Bạn có thể thực hiện việc này bằng cách xác minh rằng số lượng mục chính xác tồn tại trong RecyclerView.Adapter, hoặc bằng cách lặp lại thông qua các chế độ xem hàng riêng lẻ và xác minh rằng dữ liệu có định dạng chính xác.