自动备份和恢复测试

当用户设置新设备或重新安装您的应用时,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要植入的数据、备份文件的目录和传输模式调用 。该方法会在设备上运行以下步骤:

  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()
    }
}

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:testDebugBackupTestDefaultTestSuite

check、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。
  • 测试会清除应用的数据,并更改其使用的设备上的备份设置。我们建议使用专用测试设备。

如需了解每个版本中的更改,请参阅 备份和恢复测试版本说明。