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 wiemap()
undinsertSeparators()
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 deinRepository
. - Übergeben Sie diese
Repository
an IhrViewModel
.
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 Funktionload()
sollteMediatorResult.Success
zurückgeben und das AttributendOfPaginationReached
solltefalse
sein. - Der zweite Fall liegt vor, wenn
mockApi
eine erfolgreiche Antwort zurückgibt, die zurückgegebenen Daten aber leer sind. Die Funktionload()
sollteMediatorResult.Success
zurückgeben und das AttributendOfPaginationReached
solltetrue
sein. - Im dritten Fall wirft
mockApi
beim Abrufen der Daten eine Ausnahme aus. Die Funktionload()
sollteMediatorResult.Error
zurückgeben.
So testen Sie den ersten Fall:
- Richten Sie die
mockApi
mit den zurückzugebenden Beitragsdaten ein. - Initialisieren Sie das
RemoteMediator
-Objekt. - 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.
Empfehlungen für dich
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Seite aus Netzwerk und Datenbank
- Migration zu Paging 3
- Auslagerungsdaten laden und anzeigen