Paging-Implementierung testen

Die Implementierung der Paging-Bibliothek in Ihrer App sollte mit einer robusten Teststrategie gekoppelt sein. Sie sollten Komponenten zum Laden von Daten wie PagingSource und RemoteMediator testen, um sicherzustellen, dass sie wie erwartet funktionieren. Außerdem sollten Sie End-to-End-Tests schreiben, um zu überprüfen, ob alle Komponenten in Ihrer Paging-Implementierung ordnungsgemäß und ohne unerwartete Nebeneffekte funktionieren.

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

Tests der UI-Ebene

Mit der Paging-Bibliothek abgerufene Daten werden in der UI als Flow<PagingData<Value>> verarbeitet. Fügen Sie die Abhängigkeit paging-testing ein, um Tests zu schreiben, um zu prüfen, ob die Daten in der UI Ihren Erwartungen entsprechen. Sie enthält die Erweiterung asSnapshot() für Flow<PagingData<Value>>. Es bietet APIs in seinem Lambda-Empfänger, die eine simulierte Scroll-Interaktion ermöglichen. Sie gibt eine Standard-List<Value> zurück, die durch die simulierten Scroll-Interaktionen generiert wird. So können Sie bestätigen, dass die durchgeblätterten Daten die erwarteten Elemente enthalten, die von diesen Interaktionen generiert werden. Das 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 im folgenden Snippet gezeigt:


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

Außerdem sollten Sie Einheitentests schreiben, die alle Transformationen abdecken, die Sie auf den PagingData-Stream anwenden. Verwenden Sie die Erweiterung asPagingSourceFactory. Diese Erweiterung ist für die folgenden Datentypen verfügbar:

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

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

  • List<Value>.asPagingSourceFactory(): Wenn Sie statische Transformationen wie map() und insertSeparators() für Daten testen möchten.
  • Flow<List<Value>>.asPagingSourceFactory(): Wenn Sie testen möchten, wie sich Aktualisierungen Ihrer Daten, z. B. das Schreiben in die unterstützende Datenquelle, auf die Paging-Pipeline auswirken.

Folgen Sie dem folgenden Muster, um eine dieser Erweiterungen zu verwenden:

  • Erstellen Sie die PagingSourceFactory mit der passenden Erweiterung für Ihre Anforderungen.
  • Verwende die zurückgegebene PagingSourceFactory in einem fiktiven Element für dein Repository.
  • Übergeben Sie diese Repository an Ihr ViewModel.

ViewModel kann dann wie im vorherigen Abschnitt beschrieben getestet werden. Sehen Sie sich die folgenden 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
      }
  }
}

Um die Transformation in MyViewModel zu testen, geben Sie eine fiktive Instanz von MyRepository an, die an einen statischen List delegiert, der die umzuwandelnden 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()
}

Anschließend können Sie einen Test für die Trennzeichenlogik wie im folgenden Snippet 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, dass die Daten richtig aus den Datenquellen geladen werden. Stellen Sie fiktive Versionen von Abhängigkeiten bereit, um zu überprüfen, ob die zu testenden Komponenten isoliert voneinander funktionieren. Die Hauptkomponenten, die auf der Repository-Ebene getestet werden müssen, sind PagingSource und RemoteMediator. Die Beispiele in den folgenden Abschnitten basieren auf dem Paging mit Netzwerkbeispiel.

PagingSource-Tests

Einheitentests für Ihre PagingSource-Implementierung umfassen die Einrichtung der PagingSource-Instanz und das Laden von Daten aus ihr mit einem TestPager.

Wenn Sie die PagingSource-Instanz für Tests einrichten möchten, stellen Sie dem Konstruktor fiktive Daten zur Verfügung. So haben Sie die Kontrolle über die Daten in Ihren Tests. Im folgenden Beispiel ist der Parameter RedditApi eine Retrofit-Schnittstelle, die die Serveranfragen und die Antwortklassen definiert. Eine fiktive Version kann die Schnittstelle implementieren, alle erforderlichen Funktionen überschreiben und praktische Methoden zur Konfiguration bereitstellen, wie das fiktive Objekt in Tests reagieren soll.

Sobald die Fälschungen vorhanden sind, richten Sie die Abhängigkeiten ein und initialisieren das PagingSource-Objekt im Test. Das folgende Beispiel zeigt, wie das FakeRedditApi-Objekt mit einer Liste von Testbeiträgen initialisiert und die RedditPagingSource-Instanz getestet wird:

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 haben Sie außerdem folgende Möglichkeiten:

  • Testen Sie aufeinanderfolgende Ladevorgänge von Ihrem 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()
        }
    }

Remote-Mediator-Tests

Das Ziel der RemoteMediator-Einheitentests besteht darin, zu prüfen, ob die load()-Funktion den richtigen MediatorResult zurückgibt. Tests auf Nebenwirkungen, z. B. Daten, die in die Datenbank eingefügt werden, eignen sich besser für Integrationstests.

Bestimmen Sie zuerst, welche Abhängigkeiten Ihre RemoteMediator-Implementierung benötigt. Das folgende Beispiel zeigt eine RemoteMediator-Implementierung, die eine Raumdatenbank, eine Retrofit-Oberfläche und einen Suchstring erfordert:

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 angeben, wie im Abschnitt PagingSource-Tests gezeigt. Die Bereitstellung einer Modellversion der Raumdatenbank ist sehr aufwendig. Daher kann es einfacher sein, eine In-Memory-Implementierung der Datenbank anstelle einer vollständigen Mock-Version bereitzustellen. Da zum Erstellen einer Room-Datenbank ein Context-Objekt erforderlich ist, müssen Sie diesen RemoteMediator-Test in das Verzeichnis androidTest einfügen und mit dem AndroidJUnit4-Test-Runner ausführen, damit er Zugriff auf einen Testanwendungskontext hat. Weitere Informationen zu instrumentierten Tests finden Sie unter Instrumentierte Unittests erstellen.

Definieren Sie Teardown-Funktionen, damit zwischen den Testfunktionen kein Status nach außen tritt. 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 Fälle, die getestet werden sollten:

  • Im ersten Fall gibt mockApi gültige Daten zurück. Die Funktion load() sollte MediatorResult.Success zurückgeben und das Attribut endOfPaginationReached sollte false sein.
  • Der zweite Fall liegt vor, wenn mockApi eine erfolgreiche Antwort zurückgibt, die zurückgegebenen Daten aber leer sind. Die Funktion load() sollte MediatorResult.Success zurückgeben und das Attribut endOfPaginationReached sollte true sein.
  • Im dritten Fall wirft mockApi beim Abrufen der Daten eine Ausnahme aus. Die Funktion load() sollte MediatorResult.Error zurückgeben.

So testen Sie den ersten Fall:

  1. Richten Sie die mockApi mit den zurückzugebenden Beitragsdaten ein.
  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. Da Sie die Daten aus dem mockApi nach jedem Testlauf löschen, wird standardmäßig ein leeres Ergebnis zurückgegeben.

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

Für den letzten Test muss mockApi eine Ausnahme ausgeben, damit der Test prüfen kann, 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

Mit Einheitentests können Sie sicher sein, dass die einzelnen Seitenkomponenten isoliert funktionieren. End-to-End-Tests geben jedoch mehr Gewissheit, dass die Anwendung als Ganzes funktioniert. Für diese Tests sind noch einige simulierte Abhängigkeiten erforderlich, in der Regel decken sie jedoch den Großteil des App-Codes ab.

Im Beispiel in diesem Abschnitt wird eine Pseudo-API-Abhängigkeit verwendet, um die Verwendung des Netzwerks in Tests zu vermeiden. Die Mock API ist so konfiguriert, dass ein konsistenter Satz von Testdaten zurückgegeben wird, was zu wiederholbaren Tests führt. Entscheiden Sie anhand der Funktionsweise der einzelnen Abhängigkeiten, der Konsistenz ihrer Ausgabe und der Zuverlässigkeit Ihrer Tests, welche Abhängigkeiten gegen simulierte Implementierungen ausgetauscht werden müssen.

Schreiben Sie Ihren Code so, dass Sie simulierte Versionen Ihrer Abhängigkeiten einfach austauschen können. Im folgenden Beispiel wird eine einfache Service Locator-Implementierung verwendet, um Abhängigkeiten nach Bedarf bereitzustellen und zu ändern. In größeren Anwendungen können Sie mithilfe einer Abhängigkeitsinjektionsbibliothek wie Hilt komplexere Abhängigkeitsdiagramme verwalten.

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 im nächsten Schritt prüfen, ob die von der Pager-Implementierung zurückgegebenen Daten korrekt sind. Bei einem Test sollte geprüft werden, ob das Pager-Objekt die Standarddaten beim ersten Laden der Seite lädt. Mit einem anderen Test sollte geprüft werden, ob das Pager-Objekt zusätzliche Daten basierend auf Nutzereingaben korrekt lädt. Im folgenden Beispiel wird mit dem Test geprüft, ob das Pager-Objekt die RecyclerView.Adapter mit der richtigen Anzahl von Elementen aktualisiert, die von der API zurückgegeben werden, 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());
  });
}

Instrumentierte Tests sollten überprüfen, ob die Daten auf der Benutzeroberfläche korrekt angezeigt werden. Prüfen Sie dazu entweder, ob die richtige Anzahl von Elementen im RecyclerView.Adapter vorhanden ist, oder iterieren Sie durch die einzelnen Zeilenansichten und prüfen Sie, ob die Daten richtig formatiert sind.