Cuando los usuarios configuran un dispositivo nuevo o reinstalan tu app, Android puede restablecer los datos de la app desde una copia de seguridad en la nube o una transferencia de dispositivo a dispositivo. Si tus reglas de copia de seguridad omiten datos importantes o tu app no puede usar los datos que restablece, los usuarios pierden su configuración y progreso sin ver un error.
Las pruebas automatizadas de copia de seguridad y restablecimiento te ayudan a detectar estos problemas antes de lanzar tu app. Son pruebas de JUnit 5 que se ejecutan en tu máquina de desarrollo y controlan los dispositivos conectados a través de Android Debug Bridge (adb). Cada prueba propaga datos de la app en un dispositivo, crea una copia de seguridad de la app y la restablece a través del framework de copia de seguridad de Android y, luego, verifica que los datos estén intactos. Configuras las pruebas como un paquete de pruebas del complemento de Android para Gradle (AGP) en el archivo de compilación de tu módulo.
Cómo funciona
Un paquete de pruebas de copia de seguridad y restablecimiento tiene dos partes que funcionan juntas:
- Las pruebas del host son pruebas de JUnit 5 que se ejecutan en tu máquina de desarrollo. Usan un
BackupRestoreControllerpara cada dispositivo para propagar datos, crear una copia de seguridad de tu app y restablecerla, y verificar los resultados. - Las acciones del dispositivo son clases que implementan
BackupDeviceAction. Se ejecutan en el proceso de tu app en el dispositivo, por lo que pueden leer y escribir datos a través del código propio de tu app. Las pruebas del host llaman a las acciones del dispositivo por nombre de clase.
Cuando ejecutas el paquete, AGP compila tu app y un APK de prueba que contiene las acciones del dispositivo. Antes de cada prueba, el framework de pruebas instala ambos APKs en cada dispositivo que usa la prueba y, luego, borra los datos de tu app.
Tipos de almacenamiento integrados
En la siguiente tabla, se enumeran las StorageDomain subclases que
te permiten propagar y verificar datos sin escribir acciones del dispositivo:
| Dominio de almacenamiento | Datos |
|---|---|
StorageDomain.Preference |
Una clave en un archivo SharedPreferences |
StorageDomain.Database |
Una fila en una tabla de base de datos SQLite |
StorageDomain.TextFile |
Un archivo de texto en el almacenamiento interno de tu app |
StorageDomain.BinaryFile |
Un archivo binario en el almacenamiento interno de tu app |
Para probar otros datos, como archivos de DataStore, escribe acciones del dispositivo.
Modos de transporte
Cada copia de seguridad usa un BackupTransportMode valor. En la siguiente tabla, se enumeran los modos de transporte admitidos:
| Modo | Situación |
|---|---|
LOCAL |
Una copia de seguridad en el transporte de copia de seguridad local del dispositivo (los datos de la copia de seguridad permanecen en el dispositivo) |
CLOUD_ENCRYPTED |
Una copia de seguridad en la nube, como una guardada en la Cuenta de Google del usuario desde un dispositivo con encriptación del cliente |
CLOUD_UNENCRYPTED |
Una copia de seguridad en la nube desde un dispositivo que no tiene encriptación del cliente |
DEVICE_TO_DEVICE |
Una transferencia de dispositivo a dispositivo, por ejemplo, a través de un cable o Wi-Fi |
LOCAL funciona en cualquier dispositivo compatible. Los modos de nube y de dispositivo a dispositivo crean las mismas copias de seguridad que la acción Crear copia de seguridad de los datos de la app en Android Studio: la copia de seguridad se guarda en tu máquina de desarrollo en lugar de subirse o enviarse a otro dispositivo. Los modos de nube y de dispositivo a dispositivo requieren los Servicios de Google Play en el dispositivo y una compilación depurable de tu app. Si tus reglas de copia de seguridad dependen de la encriptación del cliente, prueba ambos modos de nube.
Requisitos
Para usar las pruebas automatizadas de copia de seguridad y restablecimiento, asegúrate de que tu entorno cumpla con los siguientes requisitos:
- Gradle versión 9.6.0 o posterior
- JDK versión 17 o posterior
- Un dispositivo o emulador con Android 12 (nivel de API 31) o versiones posteriores que esté conectado a través de adb (para probar las transferencias entre dos dispositivos, conecta dos dispositivos)
Compatibilidad
En la siguiente tabla, se enumeran las versiones mínimas y recomendadas para las pruebas automatizadas de copia de seguridad y restablecimiento:
| Componente | Versión mínima | Versión recomendada |
|---|---|---|
| Android Studio | Rabbit 2 Canary 1 | Rabbit 2 Canary 4 o versiones posteriores |
| Complemento de Android para Gradle (AGP) | 9.5.0-alpha06 | 9.5.0-alpha09 o versiones posteriores |
Bibliotecas androidx.test.backup |
1.0.0-alpha01 | 1.0.0-alpha02 o versiones posteriores |
Cada versión preliminar de AGP requiere la versión preliminar correspondiente de Android Studio. Para obtener más información, consulta Versiones preliminares de AGP.
Configura tu proyecto
Para agregar un paquete de pruebas de copia de seguridad y restablecimiento al módulo de tu app, completa los siguientes pasos:
Habilita las marcas experimentales
En el archivo gradle.properties de tu proyecto, habilita los paquetes de pruebas y las pruebas de copia de seguridad:
android.experimental.testSuiteSupport=true
android.experimental.enableBackupTest=true
Declara el paquete de pruebas
En el archivo build.gradle.kts del módulo de tu app, declara un paquete de pruebas de copia de seguridad en el bloque testOptions:
android {
testOptions {
backupTests {
create("backupTest")
}
}
}
El nombre del paquete, backupTest en este ejemplo, determina los directorios del conjunto de orígenes y los nombres de las tareas de Gradle. Puedes personalizar el paquete con las siguientes propiedades opcionales:
- Usa
backupTestLibraryVersionpara establecer la versión de lasandroidx.test.backupbibliotecas que AGP agrega al paquete, como"1.0.0-alpha02". Esta propiedad es opcional. Si la omites, AGP recurre a una versión de biblioteca predeterminada basada en la versión de AGP. - Usa
targetVariantspara especificar las variantes de compilación que prueba el paquete, comotargetVariants.add("demoDebug"). Esta propiedad es opcional. Si la omites, el paquete prueba las variantes detestBuildTypede tu módulo, que esdebugde forma predeterminada. Te recomendamos que pruebes una variante depurable porque los modos de transporte de nube y de dispositivo a dispositivo requieren una app depurable.
AGP agrega la biblioteca del host, androidx.test.backup:backup-host, y JUnit 5 a las pruebas del host. Agrega la biblioteca del dispositivo, androidx.test.backup:backup, al APK de prueba y establece el ejecutor de instrumentación del APK de prueba.
Para agregar otras dependencias, usa los bloques hostJar y testApk del paquete.
Por ejemplo, para usar Truth en tus pruebas del host, agrégalo al hostJar
bloque:
create("backupTest") {
hostJar {
dependencies {
implementation("com.google.truth:truth:1.4.2")
}
}
}
Crea los conjuntos de orígenes
Crea directorios de conjuntos de orígenes que coincidan con el nombre del paquete:
- Coloca las pruebas del host en
src/{suiteName}/test/kotlin/. - Coloca las acciones del dispositivo en
src/{suiteName}/androidTest/kotlin/.
Por ejemplo, un paquete llamado backupTest usa la siguiente estructura de directorio:
app/src/backupTest/
├── androidTest/
│ └── kotlin/com/example/myapp/ThemeActions.kt
└── test/
└── kotlin/com/example/myapp/SettingsBackupTest.kt
Verifica tu configuración de copia de seguridad
Las pruebas crean una copia de seguridad de los datos que incluye la configuración de copia de seguridad de tu app y los restablecen. Asegúrate de que la copia de seguridad esté habilitada para tu app y de que tus reglas de copia de seguridad incluyan los datos que pruebas.
Escribe pruebas de copia de seguridad y restablecimiento
Escribe pruebas del host en el conjunto de orígenes test del paquete. Cada clase de prueba necesita dos anotaciones:
@ExtendWith(BackupRestoreExtension::class)proporciona dispositivos conectados a las pruebas.@BackupRestoreConfig(applicationId = "...")establece el ID de aplicación de la app en prueba. Usa el ID de aplicación de la variante que pruebas, incluido cualquierapplicationIdSuffix.
Cada método de prueba declara un parámetro BackupRestoreController para cada dispositivo que usa.
Prueba los tipos de almacenamiento integrados
Para probar los tipos de almacenamiento integrados, llama a runBackupRestoreFlow con
los datos que se propagarán, un directorio para el archivo de copia de seguridad y un modo de transporte. El método ejecuta los siguientes pasos en el dispositivo:
- Propaga cada dominio de almacenamiento.
- Detiene la app.
- Crea una copia de seguridad de la app.
- Borra los datos de la app.
- Restablece la copia de seguridad.
- Verifica que cada dominio de almacenamiento tenga el valor semilla.
Si falla un paso, el método arroja una excepción, lo que hace que falle la prueba. En el siguiente ejemplo, se prueban una clave SharedPreferences, una fila de SQLite y un archivo interno con el transporte de copia de seguridad local:
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,
)
}
}
Ten en cuenta lo siguiente cuando definas dominios de almacenamiento:
Preferencetoma el nombre del archivoSharedPreferencessin la.xmlextensión.TextFileyBinaryFileresuelven rutas de acceso relativas en función defilesDirde tu app.Databaseescribe la fila con SQL y crea la tabla con columnasTEXTsi la tabla no existe. Si tu app usa Room, te recomendamos que pruebes el contenido de la base de datos con acciones del dispositivo que usen tus DAO para que la base de datos tenga el esquema que espera Room.
Escribe acciones del dispositivo
Para propagar o verificar datos que no cubren los tipos de almacenamiento integrados, escribe acciones del dispositivo en el conjunto de orígenes androidTest del paquete. Una acción del dispositivo implementa BackupDeviceAction y anula dos miembros:
- Establece
phaseenBackupDeviceAction.PHASE_POPULATEpara una acción que propaga datos oBackupDeviceAction.PHASE_VERIFYpara una acción que verifica datos. - Implementa
executepara ejecutar la acción conContextde tu app y los argumentos de la prueba del host. Muestra un resultado creado con elBackupDeviceActionResult.successoBackupDeviceActionResult.failuremétodo de fábrica, y pasa un mensaje de error afailure.
El método execute se ejecuta en el proceso de tu app en un subproceso en segundo plano, por lo que puede bloquearse hasta que finalicen las lecturas y escrituras.
En el siguiente ejemplo, se propaga y verifica un parámetro de configuración de tema que la app almacena con Preferences DataStore. La app define DataStore en su conjunto de orígenes principal:
// 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")
Las acciones del dispositivo usan las mismas definiciones:
// 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")
}
}
}
Para mostrar datos a la prueba del host, pasa un mapa al método success. La prueba del host lee el mapa de BackupActionResult.Success.data.
Ejecuta cada paso del flujo
Para usar acciones del dispositivo o personalizar el flujo, llama a cada paso por tu cuenta. La siguiente prueba del host ejecuta las acciones del dispositivo de la sección anterior en torno a una copia de seguridad y un restablecimiento en la nube:
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)
}
Cuando falla una acción del dispositivo, runOnDevice muestra un
BackupActionResult.Failure en lugar de arrojar una excepción,
por lo que debes verificar el resultado. Los otros pasos arrojan una excepción si fallan.
Prueba las transferencias entre dispositivos
Para probar una transferencia entre dos dispositivos, declara un parámetro BackupRestoreController para cada dispositivo. Crea una copia de seguridad de la app en el dispositivo de origen y, luego, restablece el archivo de copia de seguridad en el dispositivo de destino. En el siguiente ejemplo, se reutilizan las constantes y el auxiliar assertSuccess del ejemplo anterior:
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()
}
}
El valor role de @Device es una etiqueta que facilita la lectura de la
prueba. No afecta a qué dispositivo obtiene un parámetro. Si solo hay un dispositivo conectado, ambos parámetros usan ese dispositivo y la prueba simula la transferencia en un solo dispositivo. Borrar los datos del destino garantiza que los datos restablecidos provengan de la copia de seguridad.
Elegir dispositivos
De forma predeterminada, el framework de pruebas ordena los dispositivos en línea por número de serie y los asigna a los parámetros BackupRestoreController por posición. Por ejemplo, el primer parámetro del método de prueba obtiene el primer dispositivo y el segundo parámetro obtiene el segundo dispositivo. Si no hay ningún dispositivo para una posición, el parámetro obtiene el primer dispositivo. Las posiciones cuentan todos los parámetros del método, por lo que debes declarar los parámetros del dispositivo antes que otros parámetros, como @TempDir. La variable de entorno ANDROID_SERIAL no afecta la asignación de dispositivos.
Para elegir dispositivos, establece los valores serial o api de @Device o establece las propiedades del sistema de JVM. Las propiedades del sistema tienen prioridad sobre los valores @Device. En la siguiente tabla, se enumeran las propiedades del sistema admitidas:
| Propiedad del sistema | Descripción |
|---|---|
androidx.test.backup.device.serial |
El número de serie del dispositivo para todos los parámetros del dispositivo |
androidx.test.backup.device.serials |
Una lista separada por comas de números de serie, asignados a parámetros por posición |
androidx.test.backup.device.serial.{name} |
El número de serie de los parámetros anotados con @Device(serial = "{name}"). |
androidx.test.backup.device.api |
El nivel de API del dispositivo para todos los parámetros del dispositivo |
androidx.test.backup.device.apis |
Una lista separada por comas de niveles de API, asignados a parámetros por posición |
Pasa estas propiedades con -D cuando ejecutes las pruebas desde la línea de comandos.
Las propiedades se aplican a todas las pruebas que se ejecutan, por lo que debes usar la opción --tests para ejecutar solo las pruebas que las necesiten. Por ejemplo, el siguiente comando ejecuta ThemeTransferTest con emulator-5554 como dispositivo de origen y emulator-5556 como dispositivo de destino:
./gradlew :app:testDebugBackupTestDefaultTestSuite --tests "com.example.myapp.ThemeTransferTest" -Dandroidx.test.backup.device.serials=emulator-5554,emulator-5556Conserva los datos de la app entre pruebas
Antes de cada prueba, el framework de pruebas borra los datos de tu app en cada dispositivo que usa la prueba. Para desactivar esta opción en una clase o método de prueba, anótala con @Isolation(IsolationPolicy.MANUAL).
Escribe pruebas en Java
Los métodos BackupRestoreController son funciones de suspensión de Kotlin. Si escribes
pruebas del host en Java, usa la Async variante de cada método, como
runBackupRestoreFlowAsync, que muestra un ListenableFuture.
Ejecuta pruebas de copia de seguridad y restablecimiento
AGP crea una tarea de Gradle para cada variante que prueba el paquete, llamada test{Variant}{SuiteName}DefaultTestSuite. Para ejecutar el paquete backupTest en la variante debug, conecta tus dispositivos y ejecuta la tarea:
./gradlew :app:testDebugBackupTestDefaultTestSuiteLas tareas check, test y connectedCheck no ejecutan paquetes de pruebas de copia de seguridad y restablecimiento, por lo que debes ejecutar la tarea del paquete de forma explícita.
Para ejecutar una sola clase o método de prueba, usa la
--tests opción:
./gradlew :app:testDebugBackupTestDefaultTestSuite --tests "com.example.myapp.ThemeBackupTest"Inspecciona los informes de pruebas
Una vez que se completa una ejecución de prueba, AGP genera un informe de prueba HTML en {module}/build/reports/tests/{taskName}/index.html, como app/build/reports/tests/testDebugBackupTestDefaultTestSuite/index.html.
Problemas conocidos y limitaciones
Las pruebas automatizadas de copia de seguridad y restablecimiento tienen los siguientes problemas conocidos y limitaciones:
- La compatibilidad de Android Studio con los paquetes de pruebas aún no está disponible. El editor de código puede mostrar errores de referencia no resueltos en los archivos de origen del paquete de pruebas, incluso cuando tu proyecto esté configurado correctamente. En su lugar, compila y ejecuta pruebas de copia de seguridad y restablecimiento desde la línea de comandos con Gradle.
- Las pruebas solo se ejecutan en dispositivos y emuladores que están conectados a tu máquina de desarrollo a través de adb. No se admite Firebase Test Lab.
- Las pruebas borran los datos de tu app y cambian la configuración de copia de seguridad en los dispositivos que usan. Te recomendamos que uses dispositivos de prueba dedicados.
Para conocer los cambios en cada versión, consulta las notas de la versión de las pruebas de copia de seguridad y restablecimiento.