Tina Tang's Blog

在哪裡跌倒了,就在哪裡躺下來

0%

Android筆記(19)-測試Lists和Adapters

詳細了解測試以及如何新增測試依賴項(dependencies)。進一步練習編寫單元測試(unit test)設備測試(instrumentation test)

學習目標

  • 撰寫測試的基本概念。
  • 如何新增測試專用的 Gradle 依附元件。
  • 如何透過檢測設備測試(instrumentation tests),來測試List。

最佳做法

測試程式碼在設計上會與應用程式的商業邏輯有所不同。原因是測試不應包含任何邏輯內容;只單純進行測試而已。因此,測試不應有條件陳述式 (例如 if 或 when),或者控制流程陳述式 (例如 for 或 while)。也不得操控值或執行任何實際的運算。

有時候,您的測試可能需要其中一些項目,不過一般而言,請避免使用這些項目。由於我們想要在應用程式中測試這個邏輯類型,因此如果測試中有這類程式碼,可能就會失敗,就像應用程式的程式碼可能會失敗一樣。

我們的單元測試只應從應用程式呼叫部分測試所需的程式碼,並測試呼叫這些程式碼時所產生的程式碼值狀態UI 測試則只應測試使用者介面的預期狀態


建立測試目錄

先前的筆記中,我們已說明如何建立 androidTest 目錄來進行檢測設備測試。針對 androidTest 目錄和 test 目錄,重複這個專案的流程。這兩者的流程相同,唯一的差別是對於 test 目錄,您必須從「New Directory」下拉式選單中選取「test/java」,而不是「androidTest/java」。接著請為每個名稱為 com.example.affirmations 的新目錄建立新套件。

注意: test/java 在新版本的 Android Studio 可能已經自動建好了


建立檢測設備測試類別

在「androidTest」->「com.example.affirmations」路徑中,建立名為 AffirmationsListTests.kt 的新類別。

Dice Roller 應用程式一樣,Affirmations 只有一個活動。為了測試活動的使用者介面,我們必須指明要啟動活動。試試看能不能自己回想出做法!

  1. 在新建的類別中加入測試執行器。
1
@RunWith(AndroidJUnit4::class)
  1. 為主要活動建立活動情境規則。
1
2
@get:Rule
val activity = ActivityScenarioRule(MainActivity::class.java)
  1. Affirmations 應用程式會顯示圖片及各自的正向肯定語錄清單。使用者介面不允許與項目進行任何互動,例如點擊或滑動。因此,這個應用程式的檢測設備測試只會測試靜態資料。建立名為 scroll_to_item() 的測試方法。請記得加上 @Test 的註解。

這項測試應捲動至清單中的特定項目。我們尚未介紹這個方式,因為這需要用到專案尚未參照的方法。在繼續測試之前,需要新增一些測試依附元件。


新增檢測設備測試dependencies

您應該已大致瞭解如何在應用程式的程式碼中,新增要使用的 Gradle 依附元件。透過 Gradle,我們也能新增單元測試和檢測設備測試專用的dependencies。方法是依序前往「app」->「build.gradle」,開啟應用程式層級的 build.gradle 檔案。「依附元件(dependencies)」部分會列出三種實作依附元件的方式:implementationtestImplementationandroidTestImplementation

implementation 適用於應用程式本身會使用的依附元件,testImplementation 適用於單元測試中使用的依附元件,androidTestImplementation 則適用於檢測設備測試中使用的依附元件。

  1. 新增依附元件,允許在檢測設備測試中與 RecyclerView 互動。將下列程式庫新增為 androidTestImplementation
1
androidx.test.espresso:espresso-contrib:3.5.1

注意: 您可以在這裡找到最新版本的 espresso-contrib

dependencies看起來會像這樣:

1
2
3
4
dependencies {
...
androidTestImplementation "androidx.test.espresso:espresso-contrib:3.5.1"
}
  1. 接著點選 Sync Now(同步)。

測試 RecyclerView

  1. 專案同步處理後,請返回 AffirmationsListTests.kt 檔案。提供 ViewInteraction,以使用 onView() 執行動作。onView() 方法需要傳入 ViewMatcher。使用 withId(),確認傳入的是用於確認的 RecyclerView ID。立即在 ViewInteraction 上呼叫 perform()。也就是新加入的依附元件!現在可以傳入 RecyclerViewActions.scrollToPosition<RecyclerView.Viewholder>(9) ViewAction
1
2
3
4
onView(withId(R.id.recycler_view)).perform(
RecyclerViewActions
.scrollToPosition<RecyclerView.ViewHolder>(9)
)

瞭解這一行的語法不太重要,但還是值得看一看。RecyclerViewActions 這個名稱與跟名稱所暗示的一樣:讓您在 RecyclerView 執行測試的類別。scrollToPosition()RecyclerViewActions 類別中的靜態方法,可捲動至指定位置。這個方法會傳回「一般」內容。「一般」內容不在這個程式碼研究室的涵蓋範圍內,但在此案例中,您可以把它想成是 scrollToPosition() 方法,可傳回 RecyclerView 中的所有項目 (可能是任何內容)。

在我們的應用程式中,RecyclerView 中的項目是 ViewHolder,因此我們會在方法呼叫完成後置入一對角括號,並在其中指定 RecyclerView.ViewHolder。最後,請傳遞清單中的最後一個位置 (9)

注意: 系統已針對該位置傳遞 9 的硬式編碼值,因為此應用程式中的清單大小為靜態,所以才會這樣。如果應用程式的清單大小為動態,則不建議使用硬式編碼值,因為除了大小不明外,也無法存取 MainActivityRecyclerView 轉接器 (這是 onCreate() 方法中有內含 RecyclerView 的變數所致)。如果在 MainActivity 類別層級宣告了變數,我們可以存取該變數以取得轉接器的大小。此外,UI 測試不應一定要存取測試中活動的變數;這類方法更適合用於單元測試。

下方步驟 2 中會說明如何避免在這類測試中採用硬式編碼值。

  1. 現在已經可以捲動至 RecyclerView 的所需位置,因此請做出斷言,確保 UI 顯示的是預期資訊。確保當您捲動至最後一個項目後,系統會顯示與最終肯定相關聯的文字。請從 ViewInteraction 開始,但這次在新的 ViewMatcher 中傳遞 (在本案例中為 withText())。對於此方法,請傳送包含最後一個肯定語錄文字的字串資源。withText() 方法會根據顯示的文字來識別使用者介面元件。

對於這項元件,只需檢查元件中是否顯示所需的文字即可。方法是透過在 ViewInteraction 上呼叫 check()check() 需要 ViewAssertion,因此您可以使用 matches() 方法。最後,傳遞 isDisplayed() 方法,宣告要顯示使用者介面元件。

1
2
onView(withText(R.string.affirmation10))
.check(matches(isDisplayed()))

回到用硬式編碼的方式設定捲動目標位置的附註,有一種方式可透過 RecyclerViewActions 解決此問題。當您不確定清單長度時,可以使用 scrollTo 動作。如要使用 scrollTo 函式,您需要使用 Matcher<View!>! 來尋找特定項目。這可以包含許多項目,但若要達到這項測試的目的,請使用 withText。將其套用到您剛才編寫的測試後,程式碼看起來會像這樣:

1
2
3
4
5
6
7
8
9
10
11
12
/* 捲動到字串10的位置 */
onView(withId(R.id.recycler_view)).perform(
RecyclerViewActions
.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText(R.string.affirmation10))
)
)

/* 檢查顯示字串10文字 */
onView(withText(R.string.affirmation10))
.check(matches(isDisplayed())
)
解決 bug

原 google 課程的 code 有 bug,會執行失敗:

1
2
3
4
5
6
onView(withId(R.id.recycler_view)).perform(
RecyclerViewActions
.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText(R.string.affirmation10))
)
)

參考這篇文章,在 withText() 外用 hasDescendant() 包起來,即可執行成功。

1
2
3
4
5
6
onView(withId(R.id.recycler_view)).perform(
RecyclerViewActions
.scrollTo<RecyclerView.ViewHolder>(
withText(R.string.affirmation10)
)
)

注意:您可以參閱這篇文章,瞭解與 RecyclerViewActions 類別相關聯的多個實用函式。

現在一切已準備就緒,隨時可以執行測試。裝置或模擬器應捲動到清單底部,然後才通過測試。如要確保測試結果正確無誤,請將字串 ID 替換成 R.string.affirmation1。捲動完成後,這個字串資源就不會顯示,且測試應會失敗。

RecyclerViewActions 類別提供的方法有很多種,建議您查看可用的方法


建立本機測試類別

在「test」->「com.example.affirmations」路徑中,建立名為 AffirmationsAdapterTests.kt 的新類別。


新增本機測試dependencies

在本程式碼研究室的前半部,我們討論了三種依附元件(dependencies)實作方式,且您新增了檢測設備測試的依附元件。現在請新增本機測試的依附元件。方法是依序前往「app」->「build.gradle」,並將以下內容新增為單位測試依附元件:

1
org.mockito:mockito-core:3.12.4

dependencies應如下所示:

1
2
3
4
dependencies {
...
testImplementation "org.mockito:mockito-core:3.12.4"
}
  1. 接著點選 Sync Now(同步)。

測試 Adapter

這個應用程式本身不需要進行單元測試,因為沒有足夠的邏輯可以測試。然而,我們可以取得測試各項元件的更多經驗,為日後的測試做準備。

  1. 在單元測試類別中加入以下這行:
1
private val context = mock(Context::class.java)

mock() 方法來自我們剛才在專案中實作的程式庫。模擬是單元測試的必要部分,但不在這個程式碼研究室的範圍內。我們會在另一個程式碼研究室中詳細說明模擬功能。在 Android 中,Context 是應用程式目前狀態的結構定義,但別忘了,單元測試是在 JVM 執行,而不是在實際裝置上執行,因此沒有 Context。這個模擬方法能讓我們建立 Context 的「模擬」執行個體。這個執行個體沒有任何實際的功能,但可用來測試需要結構定義的方法。

  1. 建立名為 adapter_size() 的函式並加上註解作為測試。這項測試旨在確認 adapter 與傳遞至 adapter 的 list 兩者大小相同。執行方法是建立 ItemAdapter 的執行個體,並傳入 Datasource 類別中 loadAffirmations() 方法所傳回的 list。您也可以建立新的 list 並進行測試。如果是單元測試,最佳做法是建立測試專屬的資料,以便我們為這項測試建立自訂名單。
1
2
3
4
val data = listOf(
Affirmation(R.string.affirmation1, R.drawable.image1),
Affirmation(R.string.affirmation2, R.drawable.image2)
)
  1. 立即建立 ItemAdapter 的執行個體,並傳入在上述步驟中建立的 contextdata 變數。
1
val adapter = ItemAdapter(context, data)

Recycler view adapters 有一方法,會傳回名為 getItemCount() 的 adapter 大小。對於這個應用程式而言,方法如下:

1
2
3
4
/**
* Return the size of your dataset (invoked by the layout manager)
*/
override fun getItemCount() = dataset.size
  1. 這是應該測試的方法。這個方法的傳回值必須與您在步驟 2 中建立的 list 大小相符。使用 assertEquals() 方法,並比較 list 大小和 adapter 大小的值。
1
assertEquals("ItemAdapter is not the correct size", data.size, adapter.itemCount)

您已經熟悉 assertEquals() 方法,但建議您仔細檢查這一行。第一個參數是測試失敗時,會在測試結果中顯示的字串。第二個參數是預期的值。第三個參數是實際值。您的測試類別應如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.example.affirmations

import android.content.Context
import com.example.affirmations.adapter.ItemAdapter
import com.example.affirmations.model.Affirmation
import org.junit.Assert.assertEquals
import org.junit.Test
import org.mockito.Mockito.mock

class AffirmationsAdapterTests {

private val context = mock(Context::class.java)

@Test
fun adapter_size() {
val data = listOf(
Affirmation(R.string.affirmation1, R.drawable.image1),
Affirmation(R.string.affirmation2, R.drawable.image2)
)
val adapter = ItemAdapter(context, data)
/* 比對adapter大小和list大小 */
assertEquals("ItemAdapter is not the correct size", data.size, adapter.itemCount)

}
}

最後,執行測試應該可以看到 Tests passed。