使用者設定新裝置或重新安裝應用程式時,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 連線:
- 搭載 API 級別 36.1 以上版本的裝置或模擬器,必須使用 Google Play 服務 26.33.30 以上版本。
- 如要測試兩部裝置之間的轉移作業,請連接兩部裝置。
相容性
下表列出自動備份和還原測試的最低版本和建議版本:
| 元件 | 最低版本 | 建議版本 |
|---|---|---|
| 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-alpha03 以上版本 |
每個 AGP 預先發布版都需要搭配相應的 Android Studio 預先發布版。詳情請參閱 AGP 預先發布版。
設定專案
如要在應用程式模組中新增備份和還原測試套件,請完成下列步驟:
啟用實驗旗標
在專案的 gradle.properties 檔案中,啟用測試套件和備份測試:
android.experimental.testSuiteSupport=true
android.experimental.enableBackupTest=true
宣告測試套件
在應用程式模組的 build.gradle.kts 檔案中,於 testOptions 區塊宣告備份測試套件:
android {
testOptions {
backupTests {
create("backupTest") {
backupTestLibraryVersion = "1.0.0-alpha03"
}
}
}
}
套件名稱 (在本例中為 backupTest) 會決定來源集目錄和 Gradle 工作名稱。您可以使用下列選用屬性自訂套件:
- 使用
backupTestLibraryVersion設定 AGP 新增至套件的androidx.test.backup程式庫版本,例如"1.0.0-alpha03"。這是選用屬性。如果省略,AGP 會根據 AGP 版本,改用預設程式庫版本。本頁範例需要 1.0.0-alpha03 以上版本。 - 使用
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) 為每個測試類別加上註解,為測試提供連結裝置。測試會針對套件指定的應用程式變體執行,因此您不需要設定其應用程式 ID。
每個測試方法都會為使用的每部裝置宣告 BackupRestoreController 參數。
測試內建儲存空間類型
如要測試內建儲存空間類型,請呼叫 runBackupRestoreFlow,並提供要植入的資料、備份檔案的目錄和傳輸模式。方法會在裝置上執行下列步驟:
- 為每個儲存空間網域提供種子。
- 停止應用程式。
- 備份應用程式。
- 清除應用程式資料。
- 還原備份。
- 確認每個儲存空間網域都有種子值。
如果步驟失敗,方法會擲回例外狀況,導致測試失敗。以下範例使用本機備份傳輸測試 SharedPreferences 鍵、SQLite 資料列和內部檔案:
package com.example.myapp
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)
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.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)
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.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)
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 參數。如果測試的裝置參數多於裝置數量,額外的參數會共用第一個裝置。ANDROID_SERIAL 環境變數不會影響裝置指派。
如要選擇裝置,但不想在測試中加入序號,請使用 @Device(role = "...") 為每個裝置參數指派角色。接著,在執行測試時,使用 androidx.test.backup.device.serial.{role} 系統屬性將每個角色繫結至裝置。舉例來說,下列指令會執行套件,並將 emulator-5554 設為來源裝置,emulator-5556 設為目標裝置:
./gradlew :app:testDebugBackupTestDefaultTestSuite -Dandroidx.test.backup.device.serial.source=emulator-5554 -Dandroidx.test.backup.device.serial.target=emulator-5556繫結只會套用至具有該角色的參數。其他參數則會取得剩餘的裝置。
您也可以選擇 serial 和 api 值為 @Device 的裝置,或具有下列系統屬性的裝置,這些屬性優先於 @Device 值:
| 系統屬性 | 說明 |
|---|---|
androidx.test.backup.device.serial.{role} |
以 @Device(role = "{role}") 註解的參數,代表裝置序號。優先順序高於 serials 和 serial。 |
androidx.test.backup.device.serials |
以半形逗號分隔的序號清單,依序指派給裝置參數。 |
androidx.test.backup.device.serial |
裝置序號,用於 serials 未涵蓋的裝置參數。 |
androidx.test.backup.device.apis |
以半形逗號分隔的 API 級別清單,依序指派給裝置參數。 |
androidx.test.backup.device.api |
裝置的 API 級別,適用於 apis 未涵蓋的裝置參數。 |
請以相同方式,透過 -D 傳遞這些屬性。serial、serials、api 和 apis 屬性適用於所有執行的測試,因此請使用 --tests 選項,只執行需要這些屬性的測試。
測試還原憑證
如果應用程式使用還原憑證,請測試還原金鑰是否能在備份和還原後保留。雲端和裝置對裝置模式會備份金鑰和應用程式資料,但 LOCAL 不會。
編寫裝置動作,建立金鑰並透過 Credential Manager 取得金鑰。建立動作會傳回金鑰的憑證 ID,因此驗證動作可以檢查還原作業是否帶回相同的金鑰。在這個範例中,createRequestJson 和 getRequestJson 是應用程式傳遞至 Credential Manager 的要求 JSON 字串:
// In app/src/backupTest/androidTest/kotlin/com/example/myapp/RestoreKeyActions.kt
package com.example.myapp
import android.content.Context
import androidx.credentials.CreateRestoreCredentialRequest
import androidx.credentials.CreateRestoreCredentialResponse
import androidx.credentials.CredentialManager
import androidx.credentials.GetCredentialRequest
import androidx.credentials.GetRestoreCredentialOption
import androidx.credentials.RestoreCredential
import androidx.test.backup.BackupDeviceAction
import androidx.test.backup.BackupDeviceActionArgs
import androidx.test.backup.BackupDeviceActionResult
import kotlinx.coroutines.runBlocking
import org.json.JSONObject
class CreateRestoreKeyAction : BackupDeviceAction {
override val phase = BackupDeviceAction.PHASE_POPULATE
override fun execute(
context: Context,
args: BackupDeviceActionArgs,
): BackupDeviceActionResult {
val request = CreateRestoreCredentialRequest(createRequestJson)
val response = runBlocking {
CredentialManager.create(context).createCredential(context, request)
} as CreateRestoreCredentialResponse
val credentialId = JSONObject(response.responseJson).getString("id")
return BackupDeviceActionResult.success(mapOf("credential_id" to credentialId))
}
}
class VerifyRestoreKeyAction : BackupDeviceAction {
override val phase = BackupDeviceAction.PHASE_VERIFY
override fun execute(
context: Context,
args: BackupDeviceActionArgs,
): BackupDeviceActionResult {
val option = GetRestoreCredentialOption(getRequestJson)
val response = runBlocking {
CredentialManager.create(context)
.getCredential(context, GetCredentialRequest(listOf(option)))
}
val credential = response.credential as RestoreCredential
val credentialId = JSONObject(credential.authenticationResponseJson).getString("id")
val expected = args["credential_id"]
return if (credentialId == expected) {
BackupDeviceActionResult.success()
} else {
BackupDeviceActionResult.failure("Expected key $expected, found $credentialId")
}
}
}
下列主機測試會在來源裝置上建立金鑰、在目標裝置上還原金鑰,並在目標裝置上檢查金鑰。此方法使用「執行流程的每個步驟」中的 assertSuccess 輔助程式:
package com.example.myapp
import androidx.test.backup.host.BackupActionResult
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
const val CREATE_RESTORE_KEY = "com.example.myapp.CreateRestoreKeyAction"
const val VERIFY_RESTORE_KEY = "com.example.myapp.VerifyRestoreKeyAction"
@ExtendWith(BackupRestoreExtension::class)
class RestoreKeyTest {
@Test
fun restoreKeyTransfersToNewDevice(
@Device(role = "source") source: BackupRestoreController,
@Device(role = "target") target: BackupRestoreController,
@TempDir outputDir: Path,
): Unit = runBlocking {
val created = source.runOnDevice(CREATE_RESTORE_KEY)
created.assertSuccess()
val credentialId =
(created as BackupActionResult.Success).data.getValue("credential_id")
source.stopApp()
val backupFile = source.performBackup(
BackupTransportMode.DEVICE_TO_DEVICE,
outputDir,
)
// Also deletes any restore key on the target.
target.clearAppData()
target.performRestore(backupFile)
target.runOnDevice(VERIFY_RESTORE_KEY, mapOf("credential_id" to credentialId))
.assertSuccess()
}
}
如要建立 isCloudBackupEnabled 設為 true (預設) 的金鑰,來源裝置必須具備已啟用備份功能的 Google 帳戶,並設定螢幕鎖定。否則,Credential Manager 會擲回 E2eeUnavailableException。
與在新裝置上進行雲端還原不同,雲端模式不需要相同的 Google 帳戶或來源裝置的螢幕鎖定,且會還原 isCloudBackupEnabled 設為 false 時建立的金鑰。
在測試之間保留應用程式資料
每次測試前,測試架構都會清除測試所用裝置上的應用程式資料。如要為測試類別或方法關閉這項功能,請使用 @Isolation(IsolationPolicy.MANUAL) 加上註解。
以 Java 撰寫測試
BackupRestoreController 方法是 Kotlin 暫停函式。如果您使用 Java 編寫主機測試,請使用每個方法的 Async 變體,例如 runBackupRestoreFlowAsync,這會傳回 ListenableFuture。
執行備份與還原測試
AGP 會為套件測試的每個變體建立 Gradle 工作,名為 test{Variant}{SuiteName}DefaultTestSuite。如要針對 debug 變體執行 backupTest 套件,請連結裝置並執行以下工作:
./gradlew :app:testDebugBackupTestDefaultTestSuitecheck、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。
排解還原失敗問題
在下列情況下,雲端或裝置對裝置還原作業會失敗,並顯示錯誤訊息:
- Google Play 服務略過您的應用程式,例如應用程式 ID 開頭為
com.google.或com.android.、備份內容空白,或設定android:allowBackup="false"。如要測試這類應用程式 ID 的應用程式,請使用LOCAL模式。 - 在搭載 API 級別 36.1 以上版本的裝置或模擬器上,Google Play 服務版本低於 26.33.30。請在裝置上透過 Play 商店更新 Google Play 服務,或使用 26.33.30 以上版本的系統映像檔。
已知問題和限制
自動備份與還原測試有下列已知問題和限制:
- Android Studio 尚未支援測試套件。即使專案設定正確,程式碼編輯器仍可能在測試套件來源檔案中顯示無法解析的參照錯誤。請改用 Gradle,透過指令列建構及執行備份與還原測試。
- 測試只會在透過 ADB 連線至開發機器的裝置和模擬器上執行。不支援 Firebase Test Lab。
- 測試會清除應用程式資料,並變更所用裝置的備份設定。建議使用專用測試裝置。
如要瞭解各版本的異動,請參閱備份與還原測試版本資訊。