Sayfalandırma uygulamanızı test edin

Uygulamanızda Sayfalama kitaplığını uygulamak, güçlü bir test stratejisiyle eşlenmelidir. Beklendiği gibi çalıştıklarından emin olmak için PagingSource ve RemoteMediator gibi veri yükleme bileşenlerini test etmeniz gerekir. Ayrıca, Sayfalandırma uygulamanızdaki tüm bileşenlerin beklenmedik yan etkiler olmadan birlikte doğru bir şekilde çalıştığını doğrulamak için uçtan uca testler yazmanız gerekir.

Bu kılavuzda, Çağrı kitaplığını uygulamanızın farklı mimari katmanlarında nasıl test edeceğiniz ve Sayfalama uygulamanızın tamamı için uçtan uca testlerin nasıl yazılacağı açıklanmaktadır.

Kullanıcı arayüzü katmanı testleri

Sayfalama kitaplığıyla getirilen veriler, kullanıcı arayüzünde Flow<PagingData<Value>> olarak tüketilir. Kullanıcı arayüzündeki verilerin beklediğiniz gibi olduğunu doğrulamak amacıyla test yazmak için paging-testing bağımlılığını ekleyin. Flow<PagingData<Value>> üzerinde asSnapshot() uzantısını içerir. Lambda alıcısında alaycı kaydırma etkileşimlerine izin veren API'ler sunar. Sahte kaydırma etkileşimleri tarafından oluşturulan standart bir List<Value> döndürür. Böylece, sayfa oluşturulan verilerin bu etkileşimler tarafından oluşturulan beklenen öğeleri içerdiğini iddia edebilirsiniz. Bu, aşağıdaki snippet'te gösterilmektedir:

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

Alternatif olarak, aşağıdaki snippet'te görüldüğü gibi belirli bir koşul karşılanana kadar sayfayı aşağı kaydırabilirsiniz:


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

Dönüşümleri test etme

Ayrıca, PagingData akışına uyguladığınız tüm dönüşümleri kapsayan birim testleri de yazmanız gerekir. asPagingSourceFactory uzantısını kullanın. Bu uzantı aşağıdaki veri türlerinde kullanılabilir:

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

Kullanılacak uzantının seçimi, neyi test etmeye çalıştığınıza bağlıdır. Şunu kullan:

  • List<Value>.asPagingSourceFactory(): Veriler üzerinde map() ve insertSeparators() gibi statik dönüşümleri test etmek isterseniz
  • Flow<List<Value>>.asPagingSourceFactory(): Verilerinizde yapılan güncellemelerin (ör. yedek veri kaynağına yazma) sayfa oluşturma ardışık düzeninizi nasıl etkilediğini test etmek istiyorsanız.

Bu uzantılardan birini kullanmak için aşağıdaki düzeni izleyin:

  • İhtiyaçlarınıza uygun uzantıyı kullanarak PagingSourceFactory oluşturun.
  • Döndürülen PagingSourceFactory kodunu Repository için sahte olarak kullanın.
  • Repository kartını ViewModel cihazınıza aktarın.

Daha sonra ViewModel, bir önceki bölümde anlatıldığı gibi test edilebilir. Şunları göz önünde bulundurun: 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 ürününde dönüşümü test etmek için aşağıdaki snippet'te gösterildiği gibi, dönüştürülecek verileri temsil eden statik List öğesine yetki veren sahte bir MyRepository örneği sağlayın:

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

    private val pagingSourceFactory = items.asPagingSourceFactory()

    val pagingSource = pagingSourceFactory()
}

Ardından aşağıdaki snippet'te olduğu gibi ayırıcı mantığı için bir test yazabilirsiniz:

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

Veri katmanı testleri

Verileri veri kaynaklarınızdan uygun şekilde yüklediklerinden emin olmak için veri katmanınızdaki bileşenler için birim testleri yazın. Test edilen bileşenlerin izole halde doğru bir şekilde çalıştığını doğrulamak için bağımlılıkların sahte sürümlerini sağlayın. Depo katmanında test etmeniz gereken ana bileşenler PagingSource ve RemoteMediator'dir. Takip eden bölümlerde yer alan örnekler, Ağ Örneği ile Çağrı'ya dayanmaktadır.

Sayfa Kaynağı testleri

PagingSource uygulamanız için birim testleri, PagingSource örneğini oluşturmayı ve TestPager ile bu örnekten veri yüklemeyi içerir.

PagingSource örneğini test amacıyla ayarlamak için oluşturucuya sahte veriler sağlayın. Böylece testlerinizdeki veriler üzerinde kontrol sahibi olursunuz. Aşağıdaki örnekte RedditApi parametresi, sunucu isteklerini ve yanıt sınıflarını tanımlayan bir Retrofit arayüzüdür. Sahte bir sürüm, arayüzü uygulayabilir, gerekli tüm işlevleri geçersiz kılabilir ve sahte nesnenin testlerde nasıl tepki vereceğini yapılandırmak için kolaylık sağlayacak yöntemler sunabilir.

Sahteler oluşturulduktan sonra, bağımlılıkları ayarlayın ve testte PagingSource nesnesini başlatın. Aşağıdaki örnekte, FakeRedditApi nesnesinin test yayınları listesiyle başlatılması ve RedditPagingSource örneğinin test edilmesi gösterilmektedir:

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 ile aşağıdakileri de yapabilirsiniz:

  • PagingSource kaynağından art arda yüklemeleri test edin:
    @Test
    fun test_consecutive_loads() = runTest {

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

      assertThat(page.data)
      .containsExactlyElementsIn(testPosts)
      .inOrder()
    }
  • PagingSource öğelerinizdeki hata senaryolarını test edin:
    @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 testleri

RemoteMediator birim testlerinin amacı, load() işlevinin doğru MediatorResult değerini döndürdüğünü doğrulamaktır. Veritabanına eklenen veriler gibi yan etki testleri, entegrasyon testleri için daha uygundur.

İlk adım, RemoteMediator uygulamanızın hangi bağımlılıklara ihtiyacı olduğunu belirlemektir. Aşağıdaki örnekte Oda veritabanı, Retrofit arayüzü ve arama dizesi gerektiren bir RemoteMediator uygulaması gösterilmektedir:

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 testleri bölümünde gösterildiği gibi, Retrofit arayüzünü ve arama dizesini sağlayabilirsiniz. Room veritabanının örnek bir sürümünün sağlanması çok önemlidir. Bu nedenle, tam örnek sürüm yerine veritabanının bellek içi uygulamasını sağlamak daha kolay olabilir. Room veritabanı oluşturmak için Context nesnesi gerektiğinden, bu RemoteMediator testini androidTest dizinine yerleştirmeniz ve test uygulaması bağlamına erişebilmesi için AndroidJUnit4 test çalıştırıcısıyla yürütmeniz gerekir. Araçlı testler hakkında daha fazla bilgi için Araçlı birim testleri oluşturma bölümüne bakın.

Durumun test işlevleri arasında sızıntı yapmamasını sağlamak için sökme işlevleri tanımlayın. Bu sayede test çalıştırmaları arasında tutarlı sonuçlar elde edilir.

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

Bir sonraki adım, load() işlevini test etmektir. Bu örnekte test edilecek üç durum vardır:

  • İlk durum, mockApi işlevinin geçerli veri döndürdüğü zamandır. load() işlevi MediatorResult.Success döndürmeli ve endOfPaginationReached özelliği false olmalıdır.
  • İkinci durum ise mockApi değerinin başarılı bir yanıt döndürdüğü ancak döndürülen verilerin boş olduğu durumdur. load() işlevi MediatorResult.Success döndürmelidir ve endOfPaginationReached özelliği true olmalıdır.
  • Üçüncü durum ise mockApi ürününün verileri getirirken istisna yapmasıdır. load() işlevi, MediatorResult.Error değerini döndürmelidir.

İlk durumu test etmek için aşağıdaki adımları uygulayın:

  1. Döndürülecek yayın verileriyle mockApi kurulumunu yapın.
  2. RemoteMediator nesnesini başlatın.
  3. load() işlevini test edin.

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

İkinci testte mockApi işlevinin boş bir sonuç döndürmesi gerekir. Her test çalıştırmasından sonra mockApi içindeki verileri temizlediğiniz için varsayılan olarak boş bir sonuç döndürülür.

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

Son test, testin load() işlevinin doğru MediatorResult.Error değerini döndürdüğünü doğrulayabilmesi için mockApi öğesinin bir istisna oluşturmasını gerektirir.

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

Uçtan uca testler

Birim testleri, her bir Sayfalama bileşenlerinin yalıtım halinde çalıştığı konusunda güvence sağlar, ancak uçtan uca testler uygulamanın bir bütün olarak çalıştığına dair daha fazla güven sağlar. Bu testler için bazı örnek bağımlılıklar gerekir, ancak genellikle uygulama kodunuzun çoğunu kapsarlar.

Bu bölümdeki örnekte, ağın testlerde kullanılmaması için örnek bir API bağımlılığı kullanılmaktadır. Örnek API, tutarlı bir test verileri kümesi döndürecek şekilde yapılandırılır. Bu da tekrarlanabilir testlere olanak tanır. Her bağımlılığın ne yaptığına, çıktısının ne kadar tutarlı olduğuna ve testlerinizden ne kadar doğruluk elde etmeniz gerektiğine bağlı olarak örnek uygulamalar için hangi bağımlılıkları değiştireceğinize karar verin.

Kodunuzu, bağımlılıklarınızın sahte sürümlerini kolayca değiştirebilmenizi sağlayacak şekilde yazın. Aşağıdaki örnekte, bağımlılıkları gerektiği şekilde sağlamak ve değiştirmek için temel bir hizmet bulucu uygulaması kullanılmaktadır. Daha büyük uygulamalarda Hilt gibi bir bağımlılık yerleştirme kitaplığı kullanmak, daha karmaşık bağımlılık grafiklerini yönetmeye yardımcı olabilir.

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

Test yapısını ayarladıktan sonraki adım, Pager uygulaması tarafından döndürülen verilerin doğru olduğunu doğrulamaktır. Bir test, Pager nesnesinin sayfa ilk yüklendiğinde varsayılan verileri yüklediğini ve başka bir testin de kullanıcı girişine göre Pager nesnesinin ek verileri doğru şekilde yüklediğini doğrulaması gerekir. Aşağıdaki örnekte test, kullanıcı arama için farklı bir subreddit girdiğinde, Pager nesnesinin RecyclerView.Adapter değerini API'den döndürülen öğe sayısıyla güncellediğini doğrular.

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

Yapılan testler, verilerin kullanıcı arayüzünde doğru bir şekilde gösterildiğini doğrulamalıdır. Bu işlemi RecyclerView.Adapter içinde doğru sayıda öğe olduğunu doğrulayarak veya bağımsız satır görünümlerini tekrarlayarak ve verilerin doğru şekilde biçimlendirildiğini doğrulayarak yapabilirsiniz.