自動備份與還原測試

使用者設定新裝置或重新安裝應用程式時,Android 可以從雲端備份或裝置間傳輸還原應用程式資料。如果備份規則遺漏重要資料,或應用程式無法使用還原的資料,使用者會遺失設定和進度,且不會看到錯誤。

自動備份和還原測試可協助您在發布應用程式前找出這些問題。這類測試是 JUnit 5 測試,會在開發機器上執行,並透過 Android Debug Bridge (adb) 控制連結裝置。每項測試都會在裝置上植入應用程式資料、透過 Android 備份架構備份及還原應用程式,然後檢查資料是否完整。您可以在模組的建構檔案中,將測試設為 Android Gradle 外掛程式 (AGP) 測試套件。

運作方式

備份與還原測試套件包含兩個部分,兩者會搭配運作:

  • 主機測試是在開發機器上執行的 JUnit 5 測試。他們會為每部裝置使用 BackupRestoreController 來植入資料、備份及還原應用程式,並查看結果。
  • 裝置動作是實作 BackupDeviceAction 的類別。這些函式會在裝置上應用程式的程序中執行,因此可以透過應用程式本身的程式碼讀取及寫入資料。主機 測試會依類別名稱呼叫裝置動作。

執行套件時,AGP 會建構應用程式和包含裝置動作的測試 APK。每次測試前,測試架構都會在測試使用的每部裝置上安裝這兩個 APK,然後清除應用程式的資料。

內建儲存空間類型

下表列出可讓您植入及驗證資料,而不必編寫裝置動作的 StorageDomain 子類別:

儲存空間網域 資料
StorageDomain.Preference SharedPreferences 檔案中的金鑰。
StorageDomain.Database SQLite 資料庫資料表中的資料列。
StorageDomain.TextFile 應用程式內部儲存空間中的文字檔案。
StorageDomain.BinaryFile 應用程式內部儲存空間中的二進位檔案。

如要測試其他資料 (例如 DataStore 檔案),請編寫裝置動作。

交通方式

每個備份都會使用 BackupTransportMode 值。下表列出支援的交通方式:

模式 情境
LOCAL 備份至裝置的本機備份傳輸。備份資料會保留在裝置上。
CLOUD_ENCRYPTED 雲端備份,例如從啟用用戶端加密功能的裝置,儲存到使用者 Google 帳戶的備份。
CLOUD_UNENCRYPTED 來自未啟用用戶端加密的裝置的雲端備份。
DEVICE_TO_DEVICE 裝置對裝置轉移,例如透過傳輸線或 Wi-Fi。

LOCAL 適用於任何支援的裝置。雲端和裝置對裝置模式建立的備份,與 Android Studio 中的「Backup App Data」動作相同:備份會儲存至開發機器,而不是上傳或傳送至其他裝置。雲端模式和裝置對裝置模式都需要裝置上的 Google Play 服務,以及可偵錯的應用程式建構版本。如果備份規則採用用戶端加密,請測試這兩種雲端模式。

需求條件

如要使用自動備份和還原測試,請確認環境符合下列需求條件:

  • Gradle 9.6.0 以上版本。
  • JDK 17 以上版本。
  • 搭載 Android 12 (API 級別 31) 以上版本,並透過 adb 連線的裝置或模擬器。如要測試兩部裝置之間的傳輸作業,請連接兩部裝置。

相容性

下表列出自動備份和還原測試的最低版本和建議版本:

元件 最低版本 建議版本
Android Studio Rabbit 2 Canary 1 Rabbit 2 Canary 4 以上版本
Android Gradle 外掛程式 (AGP) 9.5.0-alpha06 9.5.0-alpha09 以上版本
androidx.test.backup 個程式庫 1.0.0-alpha01 1.0.0-alpha02 以上版本

每個 AGP 預先發布版都需要搭配相應的 Android Studio 預先發布版。詳情請參閱「AGP 預先發布版本」。

設定專案

如要在應用程式模組中新增備份與還原測試套件,請完成下列步驟:

啟用實驗性旗標

在專案的 gradle.properties 檔案中,啟用測試套件和備份測試:

android.experimental.testSuiteSupport=true
android.experimental.enableBackupTest=true

宣告測試套件

在應用程式模組的 build.gradle.kts 檔案中,於 testOptions 區塊宣告備份測試套件:

android {
    testOptions {
        backupTests {
            create("backupTest")
        }
    }
}

套件名稱 (在本例中為 backupTest) 會決定來源集目錄和 Gradle 工作名稱。您可以使用下列選用屬性自訂套件:

  • 使用 backupTestLibraryVersion 設定 AGP 新增至套件的 androidx.test.backup 程式庫版本,例如 "1.0.0-alpha02"。這是選用屬性。如果省略,AGP 會根據 AGP 版本,改用預設程式庫版本。
  • 使用 targetVariants 指定套件測試的建構變化版本,例如 targetVariants.add("demoDebug")。這項屬性為選用項目。如果省略,套件會測試模組 testBuildType 的變數,預設為 debug。建議您測試可偵錯的變化版本,因為雲端和裝置對裝置傳輸模式需要可偵錯的應用程式。

AGP 會將主機程式庫 androidx.test.backup:backup-host 和 JUnit 5 新增至主機測試。這會將裝置程式庫 androidx.test.backup:backup 新增至測試 APK,並設定測試 APK 的檢測設備執行器。

如要新增其他依附元件,請使用套件的 hostJar 和 testApk 區塊。舉例來說,如要在主機測試中使用 Truth,請將其新增至 hostJar 區塊:

create("backupTest") {
    hostJar {
        dependencies {
            implementation("com.google.truth:truth:1.4.2")
        }
    }
}

建立來源集

建立與套件名稱相符的來源集目錄:

  • 將主機測試放在 src/{suiteName}/test/kotlin/ 中。
  • 將裝置動作放入 src/{suiteName}/androidTest/kotlin/。

舉例來說,名為 backupTest 的套件會使用下列目錄結構:

app/src/backupTest/
├── androidTest/
│   └── kotlin/com/example/myapp/ThemeActions.kt
└── test/
    └── kotlin/com/example/myapp/SettingsBackupTest.kt

檢查備份設定

測試會備份及還原應用程式備份設定中包含的資料。確認應用程式已啟用備份功能,且備份規則包含您測試的資料。

編寫備份與還原測試

在套件的 test 來源集中編寫主機測試。每個測試類別都需要兩個註解:

  • @ExtendWith(BackupRestoreExtension::class) 會為測試提供連結裝置。
  • @BackupRestoreConfig(applicationId = "...") 會設定受測應用程式的應用程式 ID。使用您測試的變體應用程式 ID,包括任何 applicationIdSuffix。

每個測試方法都會為使用的每部裝置宣告 BackupRestoreController 參數。

測試內建儲存空間類型

如要測試內建儲存空間類型,請呼叫 runBackupRestoreFlow,並提供要植入的資料、備份檔案的目錄和傳輸模式。這個方法會在裝置上執行下列步驟:

  1. 為每個儲存空間網域提供種子。
  2. 停止應用程式。
  3. 備份應用程式。
  4. 清除應用程式資料。
  5. 還原備份。
  6. 確認每個儲存空間網域都有種子值。

如果步驟失敗,方法會擲回例外狀況,導致測試失敗。下列範例會使用本機備份傳輸方式,測試 SharedPreferences 金鑰、SQLite 資料列和內部檔案:

package com.example.myapp

import androidx.test.backup.host.BackupRestoreConfig
import androidx.test.backup.host.BackupRestoreController
import androidx.test.backup.host.BackupRestoreExtension
import androidx.test.backup.host.BackupTransportMode
import androidx.test.backup.host.StorageDomain
import java.nio.file.Path
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.io.TempDir

@ExtendWith(BackupRestoreExtension::class)
@BackupRestoreConfig(applicationId = "com.example.myapp")
class SettingsBackupTest {

    @Test
    fun settingsSurviveBackupAndRestore(
        device: BackupRestoreController,
        @TempDir outputDir: Path,
    ): Unit = runBlocking {
        device.runBackupRestoreFlow(
            storages = listOf(
                StorageDomain.Preference(
                    prefName = "settings",
                    key = "theme",
                    value = "dark",
                ),
                StorageDomain.Database(
                    dbName = "notes.db",
                    table = "notes",
                    primaryKeyCol = "id",
                    primaryKeyVal = 1,
                    columnValues = mapOf("title" to "Groceries"),
                ),
                StorageDomain.TextFile(
                    path = "drafts/note.txt",
                    content = "Buy milk",
                ),
            ),
            outputDir = outputDir,
            mode = BackupTransportMode.LOCAL,
        )
    }
}

定義儲存空間網域時,請注意下列事項:

  • Preference 會採用 SharedPreferences 檔案的名稱,但不含 .xml 副檔名。
  • TextFile 和 BinaryFile 會根據應用程式的filesDir解析相對路徑。
  • Database 會使用 SQL 寫入資料列,如果資料表不存在,則會建立包含 TEXT 欄的資料表。如果應用程式使用 Room,建議您使用 DAO 的裝置動作測試資料庫內容,確保資料庫具有 Room 預期的結構定義。

寫入裝置動作

如要植入或驗證內建儲存類型未涵蓋的資料,請在套件的 androidTest 來源集中編寫裝置動作。裝置動作會實作 BackupDeviceAction,並覆寫兩個成員:

  • 將 phase 設為 BackupDeviceAction.PHASE_POPULATE,用於植入資料的動作,或 BackupDeviceAction.PHASE_VERIFY,用於檢查資料的動作。
  • 實作 execute,使用應用程式的 Context 和主機測試的引數執行動作。傳回以 BackupDeviceActionResult.success 或 BackupDeviceActionResult.failure 工廠方法建立的結果,並將錯誤訊息傳遞至 failure。

execute 方法會在應用程式程序的背景執行緒中執行,因此可以封鎖,直到讀取和寫入作業完成。

以下範例會植入並驗證應用程式透過 Preferences DataStore 儲存的主題設定。應用程式會在主要來源集中定義 DataStore:

// In app/src/main/kotlin/com/example/myapp/Settings.kt
package com.example.myapp

import android.content.Context
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore

val Context.settingsDataStore by preferencesDataStore(name = "settings")
val THEME_KEY = stringPreferencesKey("theme")

裝置動作使用相同的定義:

// In app/src/backupTest/androidTest/kotlin/com/example/myapp/ThemeActions.kt
package com.example.myapp

import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.test.backup.BackupDeviceAction
import androidx.test.backup.BackupDeviceActionArgs
import androidx.test.backup.BackupDeviceActionResult
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking

class SeedThemeAction : BackupDeviceAction {
    override val phase = BackupDeviceAction.PHASE_POPULATE

    override fun execute(
        context: Context,
        args: BackupDeviceActionArgs,
    ): BackupDeviceActionResult {
        val theme = args["theme"]
            ?: return BackupDeviceActionResult.failure("Missing theme argument")
        runBlocking {
            context.settingsDataStore.edit { it[THEME_KEY] = theme }
        }
        return BackupDeviceActionResult.success()
    }
}

class VerifyThemeAction : BackupDeviceAction {
    override val phase = BackupDeviceAction.PHASE_VERIFY

    override fun execute(
        context: Context,
        args: BackupDeviceActionArgs,
    ): BackupDeviceActionResult {
        val expected = args["theme"]
        val actual = runBlocking {
            context.settingsDataStore.data.first()[THEME_KEY]
        }
        return if (actual == expected) {
            BackupDeviceActionResult.success()
        } else {
            BackupDeviceActionResult.failure("Expected $expected, found $actual")
        }
    }
}

如要將資料傳回主機測試,請將對應項傳遞至 success 方法。主機測試會從 BackupActionResult.Success.data 讀取對應。

執行流程的每個步驟

如要使用裝置動作或自訂流程,請自行呼叫每個步驟。下列主機測試會執行上一節中與雲端備份和還原相關的裝置動作:

package com.example.myapp

import androidx.test.backup.host.BackupActionResult
import androidx.test.backup.host.BackupRestoreConfig
import androidx.test.backup.host.BackupRestoreController
import androidx.test.backup.host.BackupRestoreExtension
import androidx.test.backup.host.BackupTransportMode
import java.nio.file.Path
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.fail
import org.junit.jupiter.api.io.TempDir

const val SEED_THEME = "com.example.myapp.SeedThemeAction"
const val VERIFY_THEME = "com.example.myapp.VerifyThemeAction"

@ExtendWith(BackupRestoreExtension::class)
@BackupRestoreConfig(applicationId = "com.example.myapp")
class ThemeBackupTest {

    @Test
    fun themeSurvivesCloudBackup(
        device: BackupRestoreController,
        @TempDir outputDir: Path,
    ): Unit = runBlocking {
        val args = mapOf("theme" to "dark")
        device.runOnDevice(SEED_THEME, args).assertSuccess()

        device.stopApp()
        val backupFile = device.performBackup(
            BackupTransportMode.CLOUD_ENCRYPTED,
            outputDir,
        )
        device.clearAppData()
        device.performRestore(backupFile)

        device.runOnDevice(VERIFY_THEME, args).assertSuccess()
    }
}

fun BackupActionResult.assertSuccess() {
    if (this is BackupActionResult.Failure) fail(errorMessage)
}

裝置動作失敗時,runOnDevice 會傳回 BackupActionResult.Failure,而不是擲回例外狀況,因此請檢查結果。如果失敗,其他步驟會擲回例外狀況。

測試裝置間的轉移作業

如要測試兩部裝置之間的轉移作業,請為每部裝置宣告一個 BackupRestoreController 參數。在來源裝置上備份應用程式,然後在目標裝置上還原備份檔案。以下範例重複使用上一個範例的常數和 assertSuccess 輔助函式:

package com.example.myapp

import androidx.test.backup.host.BackupRestoreConfig
import androidx.test.backup.host.BackupRestoreController
import androidx.test.backup.host.BackupRestoreExtension
import androidx.test.backup.host.BackupTransportMode
import androidx.test.backup.host.Device
import java.nio.file.Path
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.io.TempDir

@ExtendWith(BackupRestoreExtension::class)
@BackupRestoreConfig(applicationId = "com.example.myapp")
class ThemeTransferTest {

    @Test
    fun themeTransfersToNewDevice(
        @Device(role = "source") source: BackupRestoreController,
        @Device(role = "target") target: BackupRestoreController,
        @TempDir outputDir: Path,
    ): Unit = runBlocking {
        val args = mapOf("theme" to "dark")
        source.runOnDevice(SEED_THEME, args).assertSuccess()
        source.stopApp()
        val backupFile = source.performBackup(
            BackupTransportMode.DEVICE_TO_DEVICE,
            outputDir,
        )

        target.clearAppData()
        target.performRestore(backupFile)
        target.runOnDevice(VERIFY_THEME, args).assertSuccess()
    }
}

@Device 的 role 值是標籤,可讓測試更容易閱讀。不會影響參數取得的裝置。如果只連結一部裝置,這兩個參數都會使用該裝置,且測試會模擬在單一裝置上傳輸。清除目標裝置的資料,確保還原的資料來自備份。

選擇裝置

根據預設,測試架構會依序號排序線上裝置,並依位置將裝置指派給 BackupRestoreController 參數。舉例來說,測試方法的第一個參數會取得第一個裝置,第二個參數則會取得第二個裝置。如果某個位置沒有裝置,參數會取得第一個裝置。位置會計算方法的所有參數,因此請在其他參數 (例如 @TempDir) 之前宣告裝置參數。ANDROID_SERIAL 環境變數不會影響裝置指派。

如要選擇裝置,請設定 @Device 的 serial 或 api 值,或設定 JVM 系統屬性。系統屬性的優先順序高於 @Device 值。下表列出支援的系統屬性:

系統屬性 說明
androidx.test.backup.device.serial 裝置序號 (適用於所有裝置參數)。
androidx.test.backup.device.serials 以半形逗號分隔的序號清單,會依位置指派給參數。
androidx.test.backup.device.serial.{name} 使用 @Device(serial = "{name}") 註解的參數序號。
androidx.test.backup.device.api 所有裝置參數的裝置 API 級別。
androidx.test.backup.device.apis 以半形逗號分隔的 API 級別清單,會依位置指派給參數。

透過指令列執行測試時,請使用 -D 傳遞這些屬性。 這些屬性會套用至執行的每項測試,因此請使用 --tests 選項,只執行需要這些屬性的測試。舉例來說,下列指令會以 emulator-5554 做為來源裝置,並以 emulator-5556 做為目標裝置執行 ThemeTransferTest:

./gradlew :app:testDebugBackupTestDefaultTestSuite --tests "com.example.myapp.ThemeTransferTest" -Dandroidx.test.backup.device.serials=emulator-5554,emulator-5556

在測試之間保留應用程式資料

每次測試前,測試架構都會清除測試所用裝置上的應用程式資料。如要為測試類別或方法關閉這項功能,請使用 @Isolation(IsolationPolicy.MANUAL) 註解。

以 Java 撰寫測試

BackupRestoreController 方法是 Kotlin 暫停函式。如果您使用 Java 編寫主機測試,請使用各方法的 Async 變體,例如 runBackupRestoreFlowAsync,這會傳回 ListenableFuture。

執行備份與還原測試

AGP 會為套件測試的每個變化版本建立 Gradle 工作,名為 test{Variant}{SuiteName}DefaultTestSuite。如要針對 debug 變化版本執行 backupTest 套件,請連結裝置並執行以下工作:

./gradlew :app:testDebugBackupTestDefaultTestSuite

check、test 和 connectedCheck 工作不會執行備份和還原測試套件,因此請明確執行套件的工作。

如要執行單一測試類別或方法,請使用 --tests 選項:

./gradlew :app:testDebugBackupTestDefaultTestSuite --tests "com.example.myapp.ThemeBackupTest"

檢查測試報告

測試執行完畢後,AGP 會在 {module}/build/reports/tests/{taskName}/index.html 產生 HTML 測試報告,例如 app/build/reports/tests/testDebugBackupTestDefaultTestSuite/index.html。

已知問題和限制

自動備份與還原測試有下列已知問題和限制:

  • Android Studio 尚未支援測試套件。即使專案設定正確,程式碼編輯器仍可能在測試套件來源檔案中顯示無法解析的參照錯誤。改用 Gradle,透過指令列建構及執行備份和還原測試。
  • 測試只會在透過 ADB 連線至開發機器的裝置和模擬器上執行。不支援 Firebase Test Lab。
  • 測試會清除應用程式資料,並變更裝置的備份設定。建議使用專用測試裝置。

如要瞭解各版本的異動,請參閱備份與還原測試版本資訊。