Реализация библиотеки подкачки в вашем приложении должна сочетаться с надежной стратегией тестирования. Вам следует протестировать компоненты загрузки данных, такие как PagingSource
и RemoteMediator
чтобы убедиться, что они работают должным образом. Вам также следует написать сквозные тесты, чтобы убедиться, что все компоненты вашей реализации пейджинга работают правильно вместе без неожиданных побочных эффектов.
В этом руководстве объясняется, как тестировать библиотеку подкачки на разных уровнях архитектуры вашего приложения, а также как писать сквозные тесты для всей реализации подкачки.
Тесты уровня пользовательского интерфейса
Данные, полученные с помощью библиотеки подкачки, используются в пользовательском интерфейсе как Flow<PagingData<Value>>
. Чтобы написать тесты для проверки того, что данные в пользовательском интерфейсе соответствуют ожиданиям, включите зависимость paging-testing
. Он содержит расширение asSnapshot()
для Flow<PagingData<Value>>
. В своем лямбда-приемнике он предлагает API, которые позволяют имитировать взаимодействие с прокруткой. Он возвращает стандартный List<Value>
, созданный в результате имитируемых взаимодействий прокрутки, что позволяет вам утверждать, что постраничные данные содержат ожидаемые элементы, сгенерированные этими взаимодействиями. Это проиллюстрировано в следующем фрагменте:
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
)
}
Альтернативно, вы можете прокручивать до тех пор, пока не будет выполнен заданный предикат, как показано в фрагменте ниже:
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" }
}
Тестирование преобразований
Вам также следует написать модульные тесты, охватывающие любые преобразования, которые вы применяете к потоку PagingData
. Используйте расширение asPagingSourceFactory
. Это расширение доступно для следующих типов данных:
-
List<Value>
. -
Flow<List<Value>>
.
Выбор используемого расширения зависит от того, что вы пытаетесь протестировать. Использовать:
-
List<Value>.asPagingSourceFactory()
: если вы хотите протестировать статические преобразования, такие какmap()
иinsertSeparators()
для данных. -
Flow<List<Value>>.asPagingSourceFactory()
: если вы хотите проверить, как обновления ваших данных, например запись в резервный источник данных, влияют на ваш конвейер подкачки.
Чтобы использовать любое из этих расширений, следуйте следующему шаблону:
- Создайте
PagingSourceFactory
используя соответствующее расширение для ваших нужд. - Используйте возвращенный
PagingSourceFactory
в подделке для вашегоRepository
. - Передайте этот
Repository
в своюViewModel
.
Затем ViewModel
можно протестировать, как описано в предыдущем разделе. Рассмотрим следующую 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
, предоставьте поддельный экземпляр MyRepository
, который делегирует статический List
, представляющий данные, подлежащие преобразованию, как показано в следующем фрагменте:
class FakeMyRepository(): MyRepository {
private val items = (0..100).map(Any::toString)
private val pagingSourceFactory = items.asPagingSourceFactory()
val pagingSource = pagingSourceFactory()
}
Затем вы можете написать тест логики разделителя, как показано в следующем фрагменте:
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.
}
Тесты уровня данных
Напишите модульные тесты для компонентов на уровне данных, чтобы убедиться, что они правильно загружают данные из ваших источников данных. Предоставьте поддельные версии зависимостей, чтобы убедиться, что тестируемые компоненты правильно функционируют изолированно. Основными компонентами, которые необходимо протестировать на уровне репозитория, являются PagingSource
и RemoteMediator
. Примеры в последующих разделах основаны на примере разбиения по страницам с помощью сети .
Тесты PagingSource
Модульные тесты для вашей реализации PagingSource
включают настройку экземпляра PagingSource
и загрузку из него данных с помощью TestPager
.
Чтобы настроить экземпляр PagingSource
для тестирования, предоставьте конструктору поддельные данные. Это дает вам контроль над данными в ваших тестах. В следующем примере параметр RedditApi
представляет собой интерфейс Retrofit , который определяет запросы сервера и классы ответов. Поддельная версия может реализовывать интерфейс, переопределять любые необходимые функции и предоставлять удобные методы для настройки того, как поддельный объект должен реагировать в тестах.
После того, как подделки будут на месте, настройте зависимости и инициализируйте объект PagingSource
в тесте. В следующем примере демонстрируется инициализация объекта FakeRedditApi
списком тестовых сообщений и тестирование экземпляра RedditPagingSource
:
class SubredditPagingSourceTest {
private val mockPosts = listOf(
postFactory.createRedditPost(DEFAULT_SUBREDDIT),
postFactory.createRedditPost(DEFAULT_SUBREDDIT),
postFactory.createRedditPost(DEFAULT_SUBREDDIT)
)
private val fakeApi = FakeRedditApi().apply {
mockPosts.forEach { post -> addPost(post) }
}
@Test
fun loadReturnsPageWhenOnSuccessfulLoadOfItemKeyedData() = runTest {
val pagingSource = RedditPagingSource(
fakeApi,
DEFAULT_SUBREDDIT
)
val pager = TestPager(CONFIG, pagingSource)
val result = pager.refresh() as LoadResult.Page
// Write assertions against the loaded data
assertThat(result.data)
.containsExactlyElementsIn(mockPosts)
.inOrder()
}
}
TestPager
также позволяет вам делать следующее:
- Проверьте последовательные загрузки из вашего
PagingSource
:
@Test
fun test_consecutive_loads() = runTest {
val page = with(pager) {
refresh()
append()
append()
} as LoadResult.Page
assertThat(page.data)
.containsExactlyElementsIn(testPosts)
.inOrder()
}
- Проверьте сценарии ошибок в вашем
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
Целью модульных тестов RemoteMediator
является проверка того, что функция load()
возвращает правильный MediatorResult
. Тесты на побочные эффекты, такие как вставка данных в базу данных, лучше подходят для интеграционных тестов .
Первый шаг — определить, какие зависимости необходимы вашей реализации RemoteMediator
. В следующем примере демонстрируется реализация RemoteMediator
, для которой требуется база данных Room, интерфейс Retrofit и строка поиска:
Котлин
@OptIn(ExperimentalPagingApi::class) class PageKeyedRemoteMediator( private val db: RedditDb, private val redditApi: RedditApi, private val subredditName: String ) : RemoteMediator<Int, RedditPost>() { ... }
Ява
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; ... } }
Ява
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; ... } }
Вы можете предоставить интерфейс Retrofit и строку поиска, как показано в разделе тестов PagingSource . Предоставление макетной версии базы данных Room очень сложно, поэтому может быть проще предоставить реализацию базы данных в памяти вместо полной макетной версии. Поскольку для создания базы данных Room требуется объект Context
, вы должны поместить этот тест RemoteMediator
в каталог androidTest
и выполнить его с помощью средства запуска тестов AndroidJUnit4, чтобы он имел доступ к контексту тестового приложения. Дополнительные сведения об инструментальных тестах см. в разделе Создание инструментированных модульных тестов .
Определите функции удаления, чтобы гарантировать, что состояние не перетекает между тестовыми функциями. Это обеспечивает согласованность результатов между тестовыми запусками.
Котлин
@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() } }
Ява
@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(); } }
Ява
@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(); } }
Следующим шагом будет проверка функции load()
. В этом примере есть три случая для тестирования:
- Первый случай — когда
mockApi
возвращает действительные данные. Функцияload()
должна возвращатьMediatorResult.Success
, а свойствоendOfPaginationReached
должно иметь значениеfalse
. - Второй случай —
mockApi
возвращает успешный ответ, но возвращаемые данные пусты. Функцияload()
должна возвращатьMediatorResult.Success
, а свойствоendOfPaginationReached
должно иметь значениеtrue
. - Третий случай —
mockApi
выдает исключение при получении данных. Функцияload()
должна возвращатьMediatorResult.Error
.
Выполните следующие действия, чтобы проверить первый случай:
- Настройте
mockApi
с возвращаемыми данными публикации. - Инициализируйте объект
RemoteMediator
. - Проверьте функцию
load()
.
Котлин
@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 } }
Ява
@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); }
Ява
@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()); }
Второй тест требует, чтобы mockApi
возвращал пустой результат. Поскольку вы очищаете данные из mockApi
после каждого запуска теста, по умолчанию он возвращает пустой результат.
Котлин
@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 } }
Ява
@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); }
Ява
@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()); }
Для окончательного теста требуется, чтобы mockApi
выдал исключение, чтобы тест мог убедиться, что функция load()
правильно возвращает MediatorResult.Error
.
Котлин
@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 } }
Ява
@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); }
Ява
@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)); }
Сквозные тесты
Модульные тесты обеспечивают уверенность в том, что отдельные компоненты подкачки работают изолированно, но сквозные тесты обеспечивают большую уверенность в том, что приложение работает как единое целое. Этим тестам по-прежнему потребуются некоторые макетные зависимости, но обычно они охватывают большую часть кода вашего приложения.
В примере в этом разделе используется имитация зависимости API, чтобы избежать использования сети в тестах. Макетный API настроен на возврат согласованного набора тестовых данных, что обеспечивает повторяемость тестов. Решите, какие зависимости заменить на макетные реализации, исходя из того, что делает каждая зависимость, насколько последовательны ее выходные данные и какая точность вам нужна от ваших тестов.
Напишите свой код таким образом, чтобы вы могли легко заменять макеты ваших зависимостей. В следующем примере используется базовая реализация локатора служб для предоставления и изменения зависимостей по мере необходимости. В более крупных приложениях использование библиотеки внедрения зависимостей, такой как Hilt, может помочь управлять более сложными графами зависимостей.
Котлин
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 } ) } }
Ява
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; } } ); } }
Ява
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; } } ); } }
После настройки структуры теста следующим шагом будет проверка правильности данных, возвращаемых реализацией Pager
. Один тест должен гарантировать, что объект Pager
загружает данные по умолчанию при первой загрузке страницы, а другой тест должен убедиться, что объект Pager
правильно загружает дополнительные данные на основе пользовательского ввода. В следующем примере тест проверяет, что объект Pager
обновляет RecyclerView.Adapter
правильным количеством элементов, возвращаемых из API, когда пользователь входит в другой субреддит для поиска.
Котлин
@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) } }
Ява
@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()); }); }
Ява
@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()); }); }
Инструментальные тесты должны проверять правильность отображения данных в пользовательском интерфейсе. Сделайте это либо путем проверки наличия правильного количества элементов в RecyclerView.Adapter
, либо путем итерации по отдельным представлениям строк и проверки правильности форматирования данных.
Рекомендуется для вас
- Примечание: текст ссылки отображается, когда JavaScript отключен.
- Страница из сети и базы данных
- Перейти на страницу 3
- Загрузка и отображение постраничных данных