Paging-Implementierung testen

Die Implementierung der Paging-Bibliothek in Ihrer App sollte mit einem soliden Teststrategie. Testen Sie Komponenten zum Laden von Daten, z. B. PagingSource und RemoteMediator damit sie wie erwartet funktionieren. Sie sollten zudem End-to-End-Tests Prüfen, ob alle Komponenten in der Paging-Implementierung ordnungsgemäß funktionieren ohne unerwartete Nebenwirkungen.

In diesem Leitfaden wird erläutert, wie Sie die Paging-Bibliothek in den verschiedenen Architekturebenen der Anwendung und wie Sie sie programmieren, End-to-End-Tests für Ihre gesamte Paging-Implementierung.

Tests der UI-Ebene

Mit der Paging-Bibliothek abgerufene Daten werden in der Benutzeroberfläche wie folgt verarbeitet: ein Flow<PagingData<Value>>. Um Tests zu schreiben, um zu überprüfen, ob die Daten in der Benutzeroberfläche Ihren Erwartungen entsprechen, fügen Sie den Parameter paging-testing-Abhängigkeit. Sie enthält die Erweiterung asSnapshot() auf einem Flow<PagingData<Value>>. Es bietet APIs in seinem Lambda-Empfänger, die Simulations-Scrollen ermöglichen. Interaktionen. Es wird ein standardmäßiges List<Value> zurückgegeben, das durch das Scrollen erzeugt wird. simulierten Interaktionen, sodass Sie die Daten enthält die erwarteten Elemente, die bei diesen Interaktionen generiert werden. Dies wird im folgenden Snippet veranschaulicht:

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

Alternativ können Sie scrollen, bis ein bestimmtes Prädikat erfüllt ist, wie in der Abbildung Snippet unten:


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

Transformationen testen

Sie sollten auch Einheitentests schreiben, die alle Transformationen abdecken, die Sie anwenden den Stream PagingData. asPagingSourceFactory verwenden . Diese Erweiterung ist für die folgenden Datentypen verfügbar:

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

Welche Erweiterung Sie verwenden möchten, hängt davon ab, was Sie testen möchten. Verwendung:

  • List<Value>.asPagingSourceFactory(): Wenn Sie eine statische Transformationen wie map() und insertSeparators() für Daten.
  • Flow<List<Value>>.asPagingSourceFactory(): Wenn Sie testen möchten, wie Updates wie das Schreiben in die unterstützende Datenquelle, wirkt sich zu erstellen.

Wenn Sie eine dieser Erweiterungen verwenden möchten, folgen Sie dem folgenden Muster:

  • Erstellen Sie die PagingSourceFactory mit der passenden Erweiterung für Ihr Anforderungen.
  • Zurückgegebene PagingSourceFactory verwenden in eine Fälschung für dein Repository-Konto.
  • Übergib diesen Repository an dein ViewModel.

ViewModel kann dann wie im vorherigen Abschnitt beschrieben getestet werden. Sehen Sie sich die folgende ViewModel an:

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

Geben Sie eine fiktive Instanz von an, um die Transformation in MyViewModel zu testen. MyRepository, die an eine statische List delegiert, die die zu verwendenden Daten darstellt wie im folgenden Snippet gezeigt:

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

    private val pagingSourceFactory = items.asPagingSourceFactory()

    val pagingSource = pagingSourceFactory()
}

Sie können dann wie im folgenden Snippet einen Test für die Trennzeichenlogik schreiben:

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

Datenschichttests

Schreiben Sie Einheitentests für die Komponenten in der Datenschicht, um sicherzustellen, und laden Sie die Daten entsprechend aus Ihren Datenquellen. Bereitstellen gefälschte Versionen von um zu überprüfen, ob die zu testenden Komponenten Isolation. Die Hauptkomponenten, die Sie auf der Repository-Ebene testen müssen, sind: PagingSource und RemoteMediator. Die Beispiele in den folgenden Abschnitten basieren auf den Paging mit Netzwerk Leseprobe.

PagingSource-Tests

Bei Unittests für deine PagingSource-Implementierung muss Folgendes eingerichtet werden: PagingSource-Instanz und das Laden von Daten aus ihr mit einer TestPager.

Um die PagingSource-Instanz für Tests einzurichten, stellen Sie dem -Konstruktor. So haben Sie die Kontrolle über die Daten in Ihren Tests. Im folgenden Beispiel wird der RedditApi ist ein Retrofit-Parameter. Schnittstelle, die die Serveranfragen und Antwortklassen definiert. Eine fiktive Version kann die Schnittstelle implementieren, alle erforderlichen Funktionen überschreiben, und bieten praktische Methoden, um zu konfigurieren, wie das fiktive Objekt reagieren soll. Tests durchführen.

Nachdem die Fakes vorhanden sind, richten Sie die Abhängigkeiten ein und initialisieren Sie PagingSource-Objekt im Test. Im folgenden Beispiel sehen Sie, Initialisieren des FakeRedditApi-Objekts mit einer Liste von Testbeiträgen und Tests die Instanz 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()
  }
}

Mit TestPager können Sie außerdem Folgendes tun:

  • Teste aufeinanderfolgende Ladevorgänge von deinem PagingSource:
    @Test
    fun test_consecutive_loads() = runTest {

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

      assertThat(page.data)
      .containsExactlyElementsIn(testPosts)
      .inOrder()
    }
  • Testen Sie Fehlerszenarien in Ihrem 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-Tests

Das Ziel der RemoteMediator-Einheitentests besteht darin, zu überprüfen, ob der load() die korrekte MediatorResult Tests auf Nebenwirkungen, z. B. Daten, die in die Datenbank eingefügt werden, Diese Funktion ist besser für Integrationstests geeignet.

Bestimmen Sie zuerst, welche Abhängigkeiten Ihr RemoteMediator Implementierungsbedarf. Das folgende Beispiel zeigt eine RemoteMediator Implementierung, die eine Raumdatenbank, eine Retrofit-Oberfläche und eine Suche erfordert 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;
    ...
  }
}

Sie können die Retrofit-Oberfläche und den Suchstring wie in den im Abschnitt PagingSource Tests. Mock-Version bereitstellen der Room-Datenbank ist sehr beteiligt, daher kann es einfacher sein, In-Memory-Implementierung von anstelle einer vollständigen Mock-Version. Weil eine Raumdatenbank erstellt wird ein Context-Objekt erfordert, müssen Sie diesen RemoteMediator-Test im Verzeichnis androidTest ablegen und ausführen mit dem AndroidJUnit4-Test-Runner, damit dieser Zugriff auf eine Test-App hat Kontext. Weitere Informationen zu instrumentierten Tests finden Sie unter Instrumentierte Tests erstellen Unittests.

Teardown-Funktionen definieren, um sicherzustellen, dass zwischen den Tests keine Zustandslecks auftreten Funktionen. Dies sorgt für konsistente Ergebnisse zwischen den Testläufen.

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

Im nächsten Schritt testen Sie die Funktion load(). In diesem Beispiel gibt es drei zu testende Fälle:

  • Im ersten Fall gibt mockApi gültige Daten zurück. Die Funktion load() sollte MediatorResult.Success und der endOfPaginationReached-Wert sollte false sein.
  • Im zweiten Fall gibt mockApi eine erfolgreiche Antwort zurück, aber der zurückgegebene Daten leer sind. Die Funktion load() sollte Folgendes zurückgeben: MediatorResult.Success und das Attribut endOfPaginationReached true.
  • Der dritte Fall tritt ein, wenn mockApi beim Abrufen der Daten eine Ausnahme ausgibt. Die Funktion load() sollte MediatorResult.Error zurückgeben.

Führe die folgenden Schritte aus, um den ersten Fall zu testen:

  1. Richten Sie die mockApi mit den Post-Daten ein, die zurückgegeben werden sollen.
  2. Initialisieren Sie das RemoteMediator-Objekt.
  3. Testen Sie die Funktion 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());
}

Für den zweiten Test muss mockApi ein leeres Ergebnis zurückgeben. Weil du nach jedem Testlauf aus mockApi löschen, wird eine leere wird standardmäßig angezeigt.

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

Im letzten Test muss mockApi eine Ausnahme auslösen, damit der Test Prüfen Sie, ob die load()-Funktion MediatorResult.Error korrekt zurückgibt.

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

End-to-End-Tests

Einheitentests bieten die Gewissheit, dass die einzelnen Paging-Komponenten aber End-to-End-Tests bieten mehr Sicherheit, als Ganzes funktioniert. Für diese Tests sind immer noch simulierte Abhängigkeiten erforderlich, aber Sie decken in der Regel den Großteil Ihres App-Codes ab.

Im Beispiel in diesem Abschnitt wird eine fiktive API-Abhängigkeit verwendet, um die Verwendung des in Tests. Die Mock API ist so konfiguriert, dass ein konsistenter Satz von Tests zurückgegeben wird. was zu wiederholbaren Tests führt. Abhängigkeiten festlegen, die ausgetauscht werden sollen simulierte Implementierungen basierend darauf, was die einzelnen Abhängigkeiten bewirken, wie konsistent ihre Ausgabe und wie viel Genauigkeit Sie für Ihre Tests benötigen.

Schreiben Sie Ihren Code so, dass Sie problemlos Scheinversionen Ihrer Abhängigkeiten. Im folgenden Beispiel wird eine einfache Service Locator-Anwendung Implementierung, um Ihren Nutzern und die Abhängigkeiten nach Bedarf ändern. In größeren Apps mithilfe einer Abhängigkeitsinjektion eine Bibliothek wie Hilt, und komplexere Abhängigkeitsdiagramme.

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

Nachdem Sie die Teststruktur eingerichtet haben, müssen Sie prüfen, ob die Daten Pager-Implementierung zurückgegeben wird, ist korrekt. Ein Test sollte sicherstellen, Das Pager-Objekt lädt die Standarddaten, wenn die Seite zum ersten Mal geladen wird, und ein anderes sollte geprüft werden, ob das Pager-Objekt zusätzliche Daten korrekt lädt, auf Nutzereingaben. Im folgenden Beispiel wird durch den Test geprüft, ob Pager -Objekt aktualisiert RecyclerView.Adapter mit der richtigen Anzahl von Elementen der von der API zurückgegeben wird, wenn der Nutzer ein anderes Subreddit für die Suche eingibt.

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

Mit instrumentierten Tests sollte überprüft werden, ob die Daten auf der Benutzeroberfläche korrekt angezeigt werden. Das sollten Sie tun: indem Sie prüfen, ob die richtige Anzahl von Elementen im RecyclerView.Adapter oder durch Iterieren durch die einzelnen Zeilenansichten und Prüfen, ob die Daten richtig formatiert sind.