編寫自動化測試

1. 事前準備

本程式碼研究室會介紹 Android 中的自動化測試,以及如何利用自動化測試編寫可擴充的完善應用程式。您不僅會更深入掌握 UI 邏輯和商業邏輯的差異、瞭解該如何測試這兩種邏輯,最後也會學到該如何在 Android Studio 中編寫及執行自動化測試。

必要條件

  • 具備運用函式和可組合函式來編寫 Android 應用程式的能力。

課程內容

  • Android 的自動化測試有何作用。
  • 自動化測試有何重要性。
  • 本機測試的定義與用途。
  • 檢測設備測試的定義與用途。
  • 如何針對 Android 程式碼編寫本機測試。
  • 如何針對 Android 應用程式編寫檢測設備測試。
  • 如何執行自動化測試。

建構項目

  • 本機測試
  • 檢測設備測試

軟硬體需求

  • 最新版 Android Studio
  • Tip Time 應用程式的解決方案程式碼

2. 取得範例程式碼

下載程式碼:

或者,您也可以複製 GitHub 存放區的程式碼:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-tip-calculator.git
$ cd basic-android-kotlin-compose-training-tip-calculator
$ git checkout main

3. 自動化測試

就軟體而言,「測試」是指以結構化方法檢查軟體,確保其正常運作。自動化測試則是透過可執行檢查的程式碼,確認您編寫的另一段程式碼能否正確運作。

測試是應用程式開發程序中相當重要的一環。藉由對應用程式持續執行測試,您可以在公開發布應用程式前,驗證應用程式的正確性、功能行為和可用性。

此外,測試還能用來在您進行變更的過程中,持續檢查現有程式碼。

雖然手動測試幾乎總是非常重要,但 Android 系統測試通常是以自動化方式執行。在接下來的課程中,您將重點學習如何運用自動化測試來檢測應用程式的程式碼,以及應用程式本身的功能需求。在這個程式碼研究室中,您將瞭解在 Android 執行測試的基本概念。在後續的程式碼研究室中,您可進一步瞭解關於測試 Android 應用程式的進階做法。

熟悉 Android 開發流程並測試 Android 應用程式後,請定期使用應用程式程式碼撰寫測試加以演練。每當您在應用程式中建立新功能時,請務必建立測試,日後隨著應用程式增長,工作負載也會隨之減少。您也可以透過這種方式輕鬆確認應用程式是否正常運作,無須耗費太多時間手動測試應用程式。

自動化測試是所有軟體開發作業不可或缺的一環,即使在 Android 開發流程中也不例外。因此,現在就是介紹這項功能的最佳時機!

自動化測試有何重要性

起初,您可能會覺得自己的應用程式不需要測試,但實際上,無論應用程式的大小和複雜性為何,測試都是開發流程中不可或缺的一環。

為了擴充程式碼集,您需要在加入新功能的同時測試既有功能,而這麼做的前提是您已經有測試。隨著應用程式的內容增加,手動測試會比自動化測試更耗費精力。此外,當您推出正式版應用程式並擁有龐大的使用者族群後,測試就會成為至關重要的環節。舉例來說,您必須考量搭載不同 Android 版本的各類裝置。

最後您會發現,在大部分情況下,自動化測試的執行速度都明顯高於手動測試。若在發布新程式碼前進行測試,您就可以及早發現問題並修改現有程式碼,不必擔心應用程式發布後出現非預期的行為。

請注意,自動化測試是透過軟體執行,手動測試則是由人員直接操作裝置來進行。就確保使用者獲得愉快的產品體驗來說,兩者都有舉足輕重的作用。不過,自動化測試不僅準確度更高,還能充分提升團隊的工作效率 (因為不必分派人力進行測試),執行速度也比手動測試要快得多。

自動化測試類型

本機測試

本機測試是自動化測試的一種,可以直接測試一小段程式碼是否正常運作。測試項目包括函式、類別和屬性。本機測試會在工作站上執行,也就是說不需要裝置或模擬器即可在開發環境中執行。白話來說,就是本機測試會在您的電腦上執行。此外,本機測試對電腦資源的耗用程度極低,即使資源有限,同樣能快速執行。Android Studio 的內建功能可自動執行本機測試。

檢測設備測試

在 Android 開發環境中,檢測設備測試是一種 UI 測試,可用於檢測應用程式中使用 Android API 的部分,以及應用程式本身的平台 API 和服務。

與本機測試不同,UI 測試會啟動應用程式的全部或部分內容、模擬使用者操作,並檢查應用程式是否適當回應。本課程中的 UI 測試是在實體裝置或模擬器中執行。

當您在 Android 裝置上執行檢測設備測試時,測試程式碼實際上會封裝成一個獨立的 Android 應用程式套件 (APK),就和一般 Android 應用程式一樣。APK 是一個壓縮檔,其中含有在裝置或模擬器上執行該應用程式所需要的一切程式碼和檔案。測試 APK 會與一般應用程式 APK 一併安裝在裝置或模擬器上,然後針對應用程式 APK 進行測試。

4. 編寫本機測試

準備應用程式程式碼

本機測試會直接檢測應用程式程式碼中的方法,因此要測試的方法必須可供測試類別與方法使用。下列程式碼片段中的本機測試可確保 calculateTip() 方法正常運作,但由於目前的 calculateTip() 方法是 private,因此無法透過測試存取。移除 private 標示並設為 internal

MainActivity.kt

internal fun calculateTip(amount: Double, tipPercent: Double = 15.0, roundUp: Boolean): String {
    var tip = tipPercent / 100 * amount
    if (roundUp) {
        tip = kotlin.math.ceil(tip)
    }
    return NumberFormat.getCurrencyInstance().format(tip)
}
  • calculateTip() 方法前方一行的 MainActivity.kt 檔案中,新增 @VisibleForTesting 註解:
@VisibleForTesting
internal fun calculateTip(amount: Double, tipPercent: Double = 15.0, roundUp: Boolean): String {
    var tip = tipPercent / 100 * amount
    if (roundUp) {
        tip = kotlin.math.ceil(tip)
    }
    return NumberFormat.getCurrencyInstance().format(tip)
}

這會將該方法設為公開狀態,但也同時向他人表明,公開僅基於測試目的。

建立 test 目錄

在 Android 專案中,test 目錄是編寫本機測試的地方。

建立 test 目錄:

  1. 在「Project」分頁中,將檢視畫面變更為「Project」。

b9fac49a80bc59f6.png

  1. 「src」目錄上按一下滑鼠右鍵。

6cdf1a84fd2c0a25.png

  1. 選取「New」

dc9d7b82d65502a3.png

  1. 選取「Directory」

1c9115800a6f8e36.png

  1. 在「New Directory」視窗中,選取「test/java」

56f5e2df9525a230.png

  1. 按下鍵盤的 ReturnEnter 鍵。「test」目錄現在會顯示在「Project」分頁中。

60c6a44570332cab.png

「test」目錄需要的套件結構與應用程式程式碼所在的 main 目錄相同。也就是說,就像您的應用程式程式碼是寫入「main」>「java」>「com」>「example」>「tiptime」套件一樣,您的本機測試將寫入「test」>「java」>「com」>「example」>「tiptime」

test 目錄中建立這個套件結構:

  1. 在「test/java」目錄上按一下滑鼠右鍵,然後依序選取「New」>「Package」

5814cfecbebd43e1.png

  1. 在「New Package」視窗中輸入 com.example.tiptime

74fc5fbc7e051a4c.png

建立測試類別

test 套件就緒後,就可以開始編寫測試了!第一步是建立測試類別。

  1. 在「Project」分頁中,依序點選「app」>「src」>「test」,然後按一下 test 目錄旁邊的 7aeb5945d20f0dd0.png 展開箭頭。

顯示「unitTest」資料夾

  1. tiptime 目錄上按一下滑鼠右鍵,然後依序選取「New」>「Kotlin Class/File」

8c64ee6e43c62481.png

  1. 輸入 TipCalculatorTests 做為類別名稱。

8c39d1d2ac201307.png

編寫測試

如前所述,本機測試的作用是檢測應用程式中的小段程式碼。Tip Time 應用程式的主要功能是計算小費,所以應採用本機測試,確保小費計算邏輯正確運作。

如要達成這個目的,您需要直接呼叫 calculateTip() 函式,做法與在應用程式程式碼中相同。接下來,則要確保該函式傳回的值與您傳遞至函式所預期得到的值相符。

編寫自動化測試時,有一些需要注意的地方。以下是本機測試和檢測設備測試的通用概念。這些概念乍看抽象,但在完成本程式碼研究室的課程後,相信您會更能掌握這些概念。

  • 以方法的形式編寫自動化測試。
  • 使用 @Test 為方法加上註解。這樣一來,編譯器就會知道該方法是測試方法,並以相應方式執行方法。
  • 確保名稱能清楚描述測試內容和預期結果。
  • 測試方法不採用如一般應用程式方法所用的邏輯。測試方法與實作方式無關,但會嚴格檢查指定輸入內容的預期輸出內容。也就是說,測試方法只會執行一組指令來斷言應用程式的 UI 或邏輯函式正常運作。您不需要瞭解這是什麼意思,因為您稍後就會看到此效果,但請記住,測試程式碼與您慣用的應用程式程式碼可能相當不同。
  • 測試通常會以「斷言」結尾,用於確保其符合特定條件。斷言會以方法呼叫形式呈現,其名稱包含「assert」字樣。例如:assertTrue() 是 Android 測試中常用的斷言。大多數測試都會用到斷言陳述式,但實際的應用程式程式碼很少用到。

撰寫測試:

  1. 建立測試方式,測試若帳單金額為 $10 美元,20% 的小費是多少。計算結果為 $2 美元。
import org.junit.Test

class TipCalculatorTests {

   @Test
   fun calculateTip_20PercentNoRoundup() {

   }
}

提醒您,在應用程式程式碼中,來自 MainActivity.kt 檔案的 calculateTip() 方法需要三個參數。帳單金額、小費百分比,以及是否要將結果四捨五入的標記。

fun calculateTip(amount: Double, tipPercent: Double, roundUp: Boolean)

從測試中呼叫此方法時,需像在應用程式程式碼中呼叫該方法一樣傳送這些參數。

  1. calculateTip_20PercentNoRoundup() 方法中,建立兩個常數變數:一個設為 10.00 值的 amount 變數,以及設為 20.00 值的 tipPercent 變數。
val amount = 10.00
val tipPercent = 20.00
  1. 在應用程式程式碼的 MainActivity.kt 檔案內觀察下列程式碼,系統會根據裝置語言代碼套用小費金額的格式。

MainActivity.kt

...
NumberFormat.getCurrencyInstance().format(tip)
...

在測試中驗證預期的小費金額時,必須採用相同的格式。

  1. 請建立一個 expectedTip 變數,並將值設為 NumberFormat.getCurrencyInstance().format(2)

expectedTip 變數會在稍後與 calculateTip() 方法的結果進行比較。測試就是藉由這種方式確保方法能正常運作。在最後的步驟中,請將 amount 變數設為 10.00 值,並將 tipPercent 變數設為 20.00 值。10 的 20% 是 2,因此 expectedTip 變數會設為經格式化的貨幣,其值為 2。請記住,calculateTip() 方法會傳回格式化的 String 值。

  1. 搭配 amounttipPercent 變數呼叫 calculateTip() 方法,並傳送 false 引數執行四捨五入。

在此範例中,您不需將四捨五入列入考量,因為預期結果沒有考慮四捨五入的因素。

  1. 將方法呼叫的結果儲存在常數 actualTip 變數中。

到目前為止,編寫測試的方式與在應用程式程式碼中編寫一般方法沒有太大區別。不過,由於您已取得要測試的方法所傳回的值,接下來就必須以斷言判斷該值是否為正確的值。

做出斷言通常是自動化測試的最終目標,而斷言在應用程式程式碼中並不常用。在此範例中,您想要確保 actualTip 變數等於 expectedTip 變數。為此,您可以使用 JUnit 程式庫中的 assertEquals() 方法。

assertEquals() 方法接受的兩個參數分別是預期值和實際值。這兩個值相等就代表斷言和測試通過,不相等則表示斷言和測試失敗。

  1. 呼叫此 assertEquals() 方法,然後傳遞 expectedTipactualTip 變數做為參數:
import org.junit.Assert.assertEquals
import org.junit.Test
import java.text.NumberFormat

class TipCalculatorTests {

    @Test
    fun calculateTip_20PercentNoRoundup() {
        val amount = 10.00
        val tipPercent = 20.00
        val expectedTip = NumberFormat.getCurrencyInstance().format(2)
        val actualTip = calculateTip(amount = amount, tipPercent = tipPercent, false)
        assertEquals(expectedTip, actualTip)
    }
}

執行測試

完成以上步驟後,接著就可以開始測試了!

您可能已注意到,在類別名稱和測試函式的行號旁邊,原本空白的地方出現了箭頭。您可以直接點選這些箭頭來執行測試。點選方法旁邊的箭頭時,只會執行該測試方法。如果某個類別中有多個測試方法,則點選該類別旁邊的箭頭,即可執行當中所有的測試方法。

d1d3291589b08b74.png

執行測試:

  • 按一下類別宣告旁邊的箭頭,然後點選「Run ‘TipCalculatorTests'」

301a67db81194d1a.png

畫面上應顯示以下內容:

  • 邊緣處的箭頭會替換成綠色的勾號和三角形 dc22757efa3bff97.png,代表測試已通過。

ecf625f23f30a1bb.png

  • 「Run」窗格底部會顯示部分輸入內容。

畫面顯示測試已通過

  • 這代表已通過測試。

5. 編寫檢測設備測試

建立檢測目錄

檢測目錄的建立方式和本機 test 目錄差不多。

  1. 「src」目錄上按一下滑鼠右鍵,然後依序選取「New」>「Directory」

已選取目錄選單選項

  1. 在「New Directory」視窗中選取「androidTest/java」

49b436219213c56d.png

  1. 按下鍵盤的 ReturnEnter 鍵。「androidTest」目錄現在會顯示在「Project」分頁中。

已選取 Android 測試資料夾

就像 maintest 目錄有相同的套件結構,androidTest 目錄也必須有相同的套件結構。

  1. 在「androidTest/java」資料夾上按一下滑鼠右鍵,然後依序選取「New」>「Package」
  2. 在「New Package」視窗中輸入 com.example.tiptime
  3. 按下鍵盤的 ReturnEnter 鍵。「Project」分頁現在會顯示 androidTest 目錄的完整套件結構。

建立測試類別

在 Android 專案中,檢測設備測試目錄已指定為 androidTest 目錄。

建立檢測設備測試時,您需重複建立本機測試的流程,但改成在 androidTest 目錄中建立測試。

建立測試類別:

  1. 前往「Project」窗格中的 androidTest 目錄。

a627f92d40041107.png

  1. 點選每個目錄旁邊的 a30374584d86ddb6.png 展開箭頭,直到看到 tiptime 目錄為止。

7653ebbc899a26a.png

  1. tiptime 目錄上按一下滑鼠右鍵,然後依序選取「New」>「Kotlin Class/File」

69b2c4bcf72c7b1a.png

  1. 輸入 TipUITests 做為類別名稱。

8685533c87fbbea0.png

編寫測試

檢測設備測試程式碼和本機測試程式碼有極大差異。

檢測設備測試檢測的是應用程式與其 UI 的實際例項,所以您必須設定 UI 內容,做法就和您編寫 Tip Time 應用程式的程式碼時,在 MainActivity.kt 檔案的 onCreate() 方法中設置內容一樣。無論您要針對使用 Compose 建構的應用程式編寫任何檢測設備測試,都必須先完成這個步驟才能進行。

就 Tip Time 應用程式的測試而言,您將繼續編寫與 UI 元件互動的指令,進而透過 UI 測試小費計算流程。檢測設備測試的概念乍看抽象,但別擔心!以下步驟將詳細說明整個流程。

編寫測試:

  1. 建立 composeTestRule 變數,將其設為 createComposeRule() 方法的結果,並使用 Rule 加上註解:
import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule

class TipUITests {

   @get:Rule
   val composeTestRule = createComposeRule()
}
  1. 建立 calculate_20_percent_tip() 方法並使用 @Test 加上註解:
import org.junit.Test

@Test
fun calculate_20_percent_tip() {
}

編譯器知道 androidTest 目錄中帶有 @Test 註解的方法是要用於檢測設備測試,test 目錄中帶有 @Test 註解的方法則是用於本地測試。

  1. 在函式主體中呼叫 composeTestRule.setContent() 函式。這會設定 composeTestRule 的 UI 內容。
  2. 在函式的 lambda 主體中呼叫 TipTimeLayout() 函式,同時使用 lambda 主體呼叫 TipTimeTheme() 函式。
import com.example.tiptime.ui.theme.TipTimeTheme

@Test
fun calculate_20_percent_tip() {
    composeTestRule.setContent {
        TipTimeTheme {
           TipTimeLayout()
        }
    }
}

完成後,程式碼應該類似於 MainActivity.kt 檔案中為設定 onCreate() 方法的內容而編寫的程式碼。現在 UI 內容已設置完畢,接著就可以編寫與應用程式 UI 元件互動的指令了。在此應用程式中,您需要測試應用程式是否能根據輸入的帳單金額和小費百分比,顯示正確的小費值。

  1. 您可以透過 composeTestRule 將 UI 元件做為「節點」存取。常見的做法是使用 onNodeWithText() 方法存取含有特定文字的節點。使用 onNodeWithText() 方法存取帳單金額的 TextField 可組合項:
import androidx.compose.ui.test.onNodeWithText

@Test
fun calculate_20_percent_tip() {
    composeTestRule.setContent {
        TipTimeTheme {
            TipTimeLayout()
        }
    }
    composeTestRule.onNodeWithText("Bill Amount")
}

接下來,您可以呼叫 performTextInput() 方法,並傳入想要輸入的文字,用來填入 TextField 可組合項。

  1. 將帳單金額的 TextField 填入 10 值:
import androidx.compose.ui.test.performTextInput

@Test
fun calculate_20_percent_tip() {
    composeTestRule.setContent {
        TipTimeTheme {
            TipTimeLayout()
        }
    }
    composeTestRule.onNodeWithText("Bill Amount")
.performTextInput("10")
}
  1. 請使用相同的方法在 OutlinedTextField 中填入小費百分比的 20 值。
@Test
fun calculate_20_percent_tip() {
    composeTestRule.setContent {
        TipTimeTheme {
            TipTimeLayout()
        }
    }
   composeTestRule.onNodeWithText("Bill Amount")
.performTextInput("10")
   composeTestRule.onNodeWithText("Tip Percentage").performTextInput("20")
}

填入所有 TextField 可組合函式後,小費就會顯示在應用程式畫面底部的 Text 可組合函式中。

您已指示測試填入這些 TextField 可組合函式,接著則須使用斷言來確保 Text 可組合函式能顯示正確的小費。

使用 Compose 進行檢測設備測試時,可以直接針對 UI 元件呼叫斷言。可用的斷言有很多,本例中要使用的是 assertExists() 方法。顯示小費金額的 Text 可組合項應該會顯示:Tip Amount: $2.00

  1. 做出斷言,表示存在含有該文字的節點:
import java.text.NumberFormat

@Test
fun calculate_20_percent_tip() {
    composeTestRule.setContent {
        TipTimeTheme {
            Surface (modifier = Modifier.fillMaxSize()){
                TipTimeLayout()
            }
        }
    }
   composeTestRule.onNodeWithText("Bill Amount")
      .performTextInput("10")
   composeTestRule.onNodeWithText("Tip Percentage").performTextInput("20")
   val expectedTip = NumberFormat.getCurrencyInstance().format(2)
   composeTestRule.onNodeWithText("Tip Amount: $expectedTip").assertExists(
      "No node with this text was found."
   )
}

執行測試

檢測設備測試的執行流程和本機測試一樣。只要點選個別宣告旁邊空白處的箭頭,即可執行單項測試或整個測試類別。

b435bcafc02c94ef.png

  • 按一下類別宣告旁邊的箭頭,即可顯示在裝置或模擬器上執行的測試。測試完成後,您應該會看到類似下圖的輸出內容:

f878f82d3469e877.png

6. 取得解決方案程式碼

或者,您也可以複製 GitHub 存放區的程式碼:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-tip-calculator.git
$ cd basic-android-kotlin-compose-training-tip-calculator
$ git checkout test_solution

7. 結語

恭喜!您已成功編寫第一個 Android 自動化測試。測試是控管軟體品質的重要環節。往後建構 Android 應用程式時,除了編寫應用程式功能外,也別忘了編寫測試,這樣才能確保應用程式在整個開發過程中正常運作。

摘要

  • 什麼是自動化測試。
  • 自動化測試有何重要性。
  • 本機測試和檢測設備測試之間的差異
  • 編寫自動化測試的基本最佳做法。
  • 在 Android 專案中的何處能找出及放置本機測試類別和檢測設備測試類別。
  • 如何建立測試方法。
  • 如何建立本機與檢測設備測試類別。
  • 如何在本機測試和檢測設備測試中做出斷言。
  • 如何使用測試規則。
  • 如何使用 ComposeTestRule 在測試中啟動應用程式。
  • 如何在檢測設備測試中與可組合函式互動。
  • 如何執行測試。