測試分頁實作

在應用程式中實作分頁程式庫時,應搭配完善的測試策略。建議您測試 PagingSourceRemoteMediator 等資料載入元件,確保元件運作正常。此外,您也應撰寫端對端測試,確認 Paging 實作項目中的所有元件都能正常運作,而不會產生意外的副作用。

本指南說明如何在應用程式的不同架構層測試 Paging 程式庫,以及如何為整個 Paging 實作項目撰寫端對端測試。

UI 層測試

透過 Paging 程式庫擷取的資料會在 UI 中用做 Flow<PagingData<Value>>。如要撰寫測試,驗證 UI 中的資料是否符合預期,請納入 paging-testing 依附元件。這個依附元件包含 Flow<PagingData<Value>>asSnapshot() 擴充功能,其中的 lambda 接收器提供 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
  • Repository假例項中,使用傳回的 PagingSourceFactory
  • 將該 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.
}

資料層測試

請針對資料層的元件撰寫單元測試,確保這些元件能妥善從資料來源載入資料。請提供版本的依附元件,驗證受測元件能正常獨立運作。您需要測試的存放區層主要元件為 PagingSourceRemoteMediator。後續章節中的範例是以搭配使用 Paging 與網路的範例為基礎。

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 實作所需要的依附元件。以下範例說明需要 Room 資料庫、Retrofit 介面和搜尋字串的 RemoteMediator 實作:

KotlinJavaJava
@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;
   
...
 
}
}

您可以按照 PagingSource 測試一節的說明,來提供 Retrofit 介面和搜尋字串。提供 Room 資料庫的模擬版本十分複雜,因此較簡單的做法是提供資料庫的記憶體內實作,而不是完整的模擬版本。由於建立 Room 資料庫需要 Context 物件,因此您必須將此 RemoteMediator 測試放在 androidTest 目錄中,並使用 AndroidJUnit4 測試執行工具,以便存取測試應用程式的內容。如要進一步瞭解檢測設備測試,請參閱建構檢測設備單元測試

定義拆除函式,確保狀態在測試函式之間不會外洩。這樣可確保測試執行結果維持一致。

KotlinJavaJava
@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

請按照下列步驟測試第一個案例:

  1. 使用要傳回的訊息資料設定 mockApi
  2. RemoteMediator 物件初始化。
  3. 測試 load() 函式。
KotlinJavaJava
@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 中的資料,因此根據預設將會傳回空白結果。

KotlinJavaJava
@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

KotlinJavaJava
@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) 有助管理更複雜的依附元件圖表。

KotlinJavaJava
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 物件是否可根據使用者輸入內容正確載入額外資料。在以下範例中,測試會驗證當使用者輸入不同的子版 (subreddit) 進行搜尋時,Pager 物件是否可根據 API 傳回的正確項目數量更新 RecyclerView.Adapter

KotlinJavaJava
@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 中的項目數量正確,或反覆檢視個別的資料列檢視畫面,並確認資料格式正確。

目前沒有任何建議。

建議 Google 帳戶。