当用户设置新设备或重新安装您的应用时,Android 可以从云端备份或设备到设备传输中恢复应用的数据。如果您的 备份规则遗漏了重要数据,或者您的应用无法使用 其恢复的数据,用户就会丢失其设置和进度,而不会 看到错误。
自动化备份和恢复测试有助于您在发布应用之前发现这些问题。这些测试是 JUnit 5 测试,可在开发机器上运行,并通过 Android 调试桥 (adb) 控制已连接的设备。每项测试都会在设备上植入应用数据,通过 Android 备份框架备份和恢复应用,然后检查数据是否完好无损。您可以在模块的 build 文件中将 测试配置为 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 中的备份应用数据 操作相同:备份会保存到开发机器,而不是上传或发送到其他设备。云端和设备到设备模式需要在设备上安装 Google Play 服务,并且需要应用的可调试 build。如果您的备份规则依赖于客户端加密,请测试这两种云端模式。
要求
如需使用自动化备份和恢复测试,请确保您的环境满足以下要求:
- 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指定套件测试的 build 变体, 例如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要植入的数据、备份文件的目录和传输模式调用
。该方法会在设备上运行以下步骤:
- 植入每个存储网域。
- 停止应用。
- 备份应用。
- 清除应用的数据。
- 恢复备份。
- 验证每个存储网域是否具有种子值。
如果某个步骤失败,该方法会抛出异常,导致测试失败。以下示例使用本地备份传输测试 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()
}
}
role 的 @Device 值是一个标签,可让
测试更易于阅读。它不会影响参数获取的设备。如果仅连接了一台设备,则两个参数都使用该设备,并且测试会在单台设备上模拟传输。清除目标的数据可确保恢复的数据来自备份。
选择设备
默认情况下,测试框架会按序列号对在线设备进行排序,并按位置将其分配给 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:testDebugBackupTestDefaultTestSuitecheck、test 和 connectedCheck 任务不会运行备份和恢复测试套件,因此请显式运行套件的任务。
如需运行单个测试类或方法,请使用
--tests 选项:
./gradlew :app:testDebugBackupTestDefaultTestSuite --tests "com.example.myapp.ThemeBackupTest"检查测试报告
测试运行完成后,AGP 会在 {module}/build/reports/tests/{taskName}/index.html(例如 app/build/reports/tests/testDebugBackupTestDefaultTestSuite/index.html)生成 HTML 测试报告。
已知问题和限制
自动化备份和恢复测试存在以下已知问题和限制:
- Android Studio 尚不支持测试套件。即使项目配置正确,代码编辑器也可能会在测试套件源文件中显示未解析的引用错误。请改用 Gradle 从命令行构建和运行备份和恢复测试。
- 测试仅在通过 adb 连接到开发机器的设备和模拟器上运行。不支持 Firebase Test Lab。
- 测试会清除应用的数据,并更改其使用的设备上的备份设置。我们建议使用专用测试设备。
如需了解每个版本中的更改,请参阅 备份和恢复测试版本说明。