Pruebas automatizadas de copia de seguridad y restablecimiento

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 perderán 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 la 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 y restablece la app a través del framework de copia de seguridad de Android, y, luego, verifica que los datos estén intactos. Configura 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 de host son pruebas de JUnit 5 que se ejecutan en tu máquina de desarrollo. Usan un BackupRestoreController para cada dispositivo con el objetivo de propagar datos, crear copias de seguridad y restablecer tu app, 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 de tu app. Las pruebas del host llaman a las acciones del dispositivo por nombre de clase.

Cuando ejecutas el paquete, el AGP compila tu app y un APK de prueba que contiene tus acciones del dispositivo. El framework de pruebas instala ambos APK en cada dispositivo que usan las pruebas y borra los datos de tu app antes de cada prueba.

Tipos de almacenamiento integrado

En la siguiente tabla, se enumeran las subclases de StorageDomain 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.DataStorePreference Es una clave en un archivo de Preferences DataStore.
StorageDomain.Database Es 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
StorageDomain.ExcludedData Otro dominio de almacenamiento integrado cuyos datos no se deben restablecer, como los datos excluidos por tus reglas de copia de seguridad.

Para probar otros datos, como los archivos de Proto DataStore o las bases de datos de Room, escribe acciones del dispositivo.

Medios de transporte

Cada copia de seguridad usa un valor BackupTransportMode. 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 que se guarda en la Cuenta de Google del usuario desde un dispositivo con encriptación del cliente
CLOUD_UNENCRYPTED Una copia de seguridad en la nube de 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 Backup App Data 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 9.6.0 o una versión posterior
  • JDK versión 17 o posterior
  • Un dispositivo o emulador que ejecute Android 12 (nivel de API 31) o una versión posterior, y que esté conectado a través de adb:
    • Los dispositivos o emuladores con el nivel de API 36.1 o posterior requieren la versión 26.33.30 o posterior de los Servicios de Google Play.
    • 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 una versión posterior
Complemento de Android para Gradle (AGP) 9.5.0-alpha06 9.5.0-alpha09 o una versión posterior
androidx.test.backup bibliotecas 1.0.0-alpha01 1.0.0-alpha04 o una versión posterior

Cada versión preliminar del AGP requiere la versión preliminar correspondiente de Android Studio. Para obtener más información, consulta Versiones preliminares del 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") {
                backupTestLibraryVersion = "1.0.0-alpha04"
            }
        }
    }
}

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 backupTestLibraryVersion para establecer la versión de las bibliotecas de androidx.test.backup que el AGP agrega al paquete, como "1.0.0-alpha04". Esta propiedad es opcional. Si la omites, AGP recurre a una versión de biblioteca predeterminada según la versión de AGP. Los ejemplos de esta página requieren la versión 1.0.0-alpha04 o una posterior.
  • Usa targetVariants para especificar las variantes de compilación que prueba el paquete, como targetVariants.add("demoDebug"). Esta propiedad es opcional. Si la omites, el paquete probará las variantes de la testBuildType de tu módulo, que es debug de forma predeterminada. Te recomendamos que pruebes una variante depurable, ya que los modos de transporte de nube y de dispositivo a dispositivo requieren una app depurable.

El AGP agrega la biblioteca de host, androidx.test.backup:backup-host y JUnit 5 a las pruebas de host. Agrega la biblioteca de dispositivos, androidx.test.backup:backup, al APK de prueba y configura 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 de host, agrégalo al bloque hostJar:

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 la configuración de la copia de seguridad

Las pruebas crean una copia de seguridad y restablecen los datos que incluye la configuración de copia de seguridad de tu app. Asegúrate de que la copia de seguridad esté habilitada para tu app y que tus reglas de copia de seguridad incluyan los datos que pruebes.

Escribe pruebas de copia de seguridad y restablecimiento

Escribe pruebas de host en el conjunto de orígenes test del paquete. Anota cada clase de prueba con @ExtendWith(BackupRestoreExtension::class), que proporciona dispositivos conectados a las pruebas. Las pruebas se ejecutan en la variante de la app a la que se orienta el paquete, por lo que no es necesario que configures su ID de aplicación.

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:

  1. Propaga cada dominio de almacenamiento.
  2. Detiene la app.
  3. Crea una copia de seguridad de la app.
  4. Borra los datos de la app.
  5. Restablece la copia de seguridad.
  6. Verifica que cada dominio de almacenamiento tenga el valor semilla o esté ausente cuando se une en StorageDomain.ExcludedData.

Si un paso falla, el método arroja una excepción, lo que hace que la prueba falle. En el siguiente ejemplo, se prueban SharedPreferences, Preferences DataStore, SQLite, un archivo interno y un archivo de exclusión con el transporte de copia de seguridad local:

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 = "channels",
                    value = setOf("news", "alerts"),
                ),
                StorageDomain.DataStorePreference(
                    dataStoreName = "user_prefs",
                    key = "font_scale",
                    value = 1.25f,
                ),
                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",
                ),
                // Check that a file excluded by backup rules isn't restored.
                StorageDomain.ExcludedData(
                    StorageDomain.TextFile(
                        path = "auth/session.txt",
                        content = "temp-token",
                    )
                ),
            ),
            outputDir = outputDir,
            mode = BackupTransportMode.LOCAL,
        )
    }
}

Ten en cuenta lo siguiente cuando definas los dominios de almacenamiento:

  • Preference toma el nombre del archivo SharedPreferences sin la extensión .xml.
  • DataStorePreference toma el nombre que se pasa a preferencesDataStore. Usa el mismo tipo de Kotlin para value que la clave de preferencia de tu app, como 1.25f para floatPreferencesKey, de modo que tu app pueda leer la entrada restablecida.
  • TextFile y BinaryFile resuelven las rutas de acceso relativas en función del filesDir de tu app.
  • Database escribe la fila con SQL y crea la tabla con columnas TEXT si la tabla no existe. Si tu app usa Room, prueba el contenido de la base de datos con acciones del dispositivo que usen tus DAO para que la base de datos coincida con el esquema de Room.
  • ExcludedData requiere al menos un dominio de almacenamiento no excluido en la misma llamada a runBackupRestoreFlow.
  • Preference, DataStorePreference, Database, TextFile y BinaryFile se orientan al almacenamiento protegido por el dispositivo en lugar del almacenamiento predeterminado de tu app cuando configuras isDeviceProtectedStorage = true.

Escribir 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. Implementa BackupDeviceAction y anula execute para ejecutar la acción con el Context de tu app y los argumentos de la prueba del host. Muestra un BackupDeviceActionResult creado con BackupDeviceActionResult.success o BackupDeviceActionResult.failure, y pasa un mensaje de error a failure.

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 operaciones de lectura y escritura. Para acceder a la instrumentación del ejecutor de pruebas, como UiAutomation, llama a InstrumentationRegistry.getInstrumentation.

En el siguiente ejemplo, se inicializa y verifica un parámetro de configuración de tema en un Proto DataStore, en el que settingsDataStore es una propiedad DataStore<Settings> definida en tu app:

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

import android.content.Context
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 fun execute(
        context: Context,
        args: BackupDeviceActionArgs,
    ): BackupDeviceActionResult {
        val theme = args["theme"]
            ?: return BackupDeviceActionResult.failure("Missing theme argument")
        runBlocking {
            context.settingsDataStore.updateData {
                it.toBuilder().setTheme(theme).build()
            }
        }
        return BackupDeviceActionResult.success()
    }
}

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

Para ejecutar acciones de propagación y verificación en runBackupRestoreFlow, pasa sus nombres de clase a StorageDomain.CustomData:

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

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 {
        device.runBackupRestoreFlow(
            storage = StorageDomain.CustomData(
                populateAction = SEED_THEME,
                verifyAction = VERIFY_THEME,
                args = mapOf("theme" to "dark"),
            ),
            outputDir = outputDir,
            mode = BackupTransportMode.CLOUD_ENCRYPTED,
        )
    }
}

Para pasar los valores generados durante la propagación a la acción de verificación, pasa un mapa a BackupDeviceActionResult.success. CustomData combina esas entradas en los argumentos de la acción de verificación.

Ejecuta cada paso del flujo

Para personalizar el flujo, por ejemplo, llamar a uninstallApp y installApk para probar una actualización, o llamar a launchApp, ejecuta cada paso por tu cuenta:

package com.example.myapp

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.io.TempDir

@ExtendWith(BackupRestoreExtension::class)
class CustomFlowBackupTest {

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

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

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

Cuando falla una acción del dispositivo, runOnDevice devuelve un BackupActionResult.Failure en lugar de arrojar una excepción. Llama a getOrThrow para obtener el mapa data que se devuelve en caso de éxito o arroja una IOException en caso de error.

Prueba las transferencias entre dispositivos

Para probar una transferencia entre dos dispositivos, declara un parámetro BackupRestoreController para cada dispositivo y pasa el dispositivo de destino a restoreOn en runBackupRestoreFlow:

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 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 ThemeTransferTest {

    @Test
    fun themeTransfersToNewDevice(
        @Device(role = "source") source: BackupRestoreController,
        @Device(role = "target") target: BackupRestoreController,
        @TempDir outputDir: Path,
    ): Unit = runBlocking {
        source.runBackupRestoreFlow(
            storage = StorageDomain.DataStorePreference(
                dataStoreName = "settings",
                key = "theme",
                value = "dark",
            ),
            outputDir = outputDir,
            mode = BackupTransportMode.DEVICE_TO_DEVICE,
            restoreOn = target,
        )
    }
}

En source, el flujo inicializa y crea una copia de seguridad de la app. En target, borra los datos de la app, restablece la copia de seguridad y verifica los datos. Para restablecer los datos en otro dispositivo, se requiere DEVICE_TO_DEVICE o un modo de transporte en la nube, ya que los datos de la copia de seguridad de LOCAL permanecen en el dispositivo de origen.

Los valores role de @Device te permiten elegir un dispositivo para cada parámetro cuando ejecutas las pruebas. Si solo hay un dispositivo coincidente conectado, ambos parámetros comparten ese dispositivo y simulan la transferencia en un solo dispositivo.

Elegir dispositivos

De forma predeterminada, el framework de pruebas ordena los dispositivos en línea por número de serie y asigna a cada parámetro BackupRestoreController su propio dispositivo coincidente cuando hay suficientes dispositivos conectados. Los parámetros que no pueden obtener un dispositivo independiente comparten el primer dispositivo coincidente. La variable de entorno ANDROID_SERIAL no afecta la asignación de dispositivos.

Para elegir dispositivos sin colocar números de serie en tus pruebas, asigna un rol a cada parámetro de dispositivo con @Device(role = "..."). Luego, cuando ejecutes las pruebas, vincula cada rol a un dispositivo con la propiedad del sistema androidx.test.backup.device.serial.{role}. Por ejemplo, el siguiente comando ejecuta el paquete con emulator-5554 como dispositivo de origen y emulator-5556 como dispositivo de destino:

./gradlew :app:testDebugBackupTestDefaultTestSuite -Dandroidx.test.backup.device.serial.source=emulator-5554 -Dandroidx.test.backup.device.serial.target=emulator-5556

Una vinculación solo se aplica a los parámetros con ese rol. Otros parámetros obtienen los dispositivos coincidentes restantes.

También puedes filtrar dispositivos con serial, api, minApi y maxApi en @Device, como @Device(role = "target", minApi = 35). Para establecer números de serie o niveles de API exactos desde la línea de comandos, pasa las siguientes propiedades del sistema con -D:

Propiedad del sistema Descripción
androidx.test.backup.device.serial.{role} Es el número de serie del dispositivo para los parámetros anotados con @Device(role = "{role}"). Tiene prioridad sobre serials, serial y el valor serial de @Device.
androidx.test.backup.device.serials Es una lista separada por comas de números de serie, asignados a los parámetros del dispositivo en orden. Tiene prioridad sobre serial y el valor serial de @Device.
androidx.test.backup.device.serial El número de serie de los parámetros del dispositivo que no cubre serials. Tiene prioridad sobre el valor serial de @Device.
androidx.test.backup.device.apis Es una lista de niveles de API separados por comas, asignados a los parámetros del dispositivo en orden. Usa 0 para que coincida con cualquier nivel de API. Reemplaza api por @Device, dentro de minApi y maxApi.
androidx.test.backup.device.api Es el nivel de API para los parámetros del dispositivo que no cubre apis. Usa 0 para que coincida con cualquier nivel de API. Reemplaza api por @Device, dentro de minApi y maxApi.

Las propiedades serial, serials, api y apis se aplican a cada prueba que se ejecuta, por lo que debes usar la opción --tests para ejecutar solo las pruebas que las necesitan.

Prueba las credenciales de restablecimiento

Si tu app usa Restore Credentials, prueba que su clave de restablecimiento sobreviva a una copia de seguridad y un restablecimiento. Los modos de nube y de dispositivo a dispositivo crean una copia de seguridad de la clave con los datos de tu app, pero LOCAL no lo hace.

Escribe acciones del dispositivo que creen la clave y la obtengan con Credential Manager. La acción de creación devuelve el ID de credencial de la clave para que la acción de verificación pueda comprobar que el restablecimiento devuelva la misma clave. En este ejemplo, createRequestJson y getRequestJson son las cadenas JSON de solicitud que tu app pasa al Administrador de credenciales:

// 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 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 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")
        }
    }
}

La siguiente prueba de host crea la clave en el dispositivo de origen, la restablece en el dispositivo de destino y verifica que el ID de credencial coincida:

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

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 {
        // Clears target (including any existing restore key) before restoring.
        source.runBackupRestoreFlow(
            storage = StorageDomain.CustomData(
                populateAction = CREATE_RESTORE_KEY,
                verifyAction = VERIFY_RESTORE_KEY,
            ),
            outputDir = outputDir,
            mode = BackupTransportMode.DEVICE_TO_DEVICE,
            restoreOn = target,
        )
    }
}

Para crear una clave con isCloudBackupEnabled configurado como true, el valor predeterminado, el dispositivo de origen necesita una Cuenta de Google con la copia de seguridad activada y un bloqueo de pantalla. De lo contrario, el Administrador de credenciales arroja E2eeUnavailableException.

A diferencia de un restablecimiento en la nube en un dispositivo nuevo, los modos de nube no requieren la misma Cuenta de Google ni el bloqueo de pantalla del dispositivo de origen, y restablecen las claves creadas con isCloudBackupEnabled establecido en false.

Conserva 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 un 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 de host en Java, usa la variante Async de cada método, como runBackupRestoreFlowAsync, que devuelve 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:testDebugBackupTestDefaultTestSuite

Las 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 un método de prueba, usa la opción --tests:

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

Inspecciona los informes de pruebas

Una vez que se completa una ejecución de prueba, el AGP genera un informe de prueba en formato HTML en {module}/build/reports/tests/{taskName}/index.html, como app/build/reports/tests/testDebugBackupTestDefaultTestSuite/index.html.

Cuando una prueba falla, el framework de pruebas guarda el logcat de cada dispositivo que usó la prueba y enumera las rutas de acceso de los archivos guardados en el resultado de la falla. Para desactivar el guardado de registros, pasa -Dandroidx.test.backup.device.logs.enabled=false.

Soluciona problemas de restablecimientos con errores

Un restablecimiento de la nube o de dispositivo a dispositivo falla con un mensaje de error en los siguientes casos:

  • Los Servicios de Google Play omiten tu app, por ejemplo, porque su ID de aplicación comienza con com.google. o com.android., su copia de seguridad está vacía o establece android:allowBackup="false". Para probar una app con ese ID de aplicación, usa el modo LOCAL.
  • La versión de los Servicios de Google Play es anterior a la 26.33.30 en un dispositivo o emulador que ejecuta el nivel de API 36.1 o una versión posterior. Actualiza los Servicios de Google Play a través de Play Store en el dispositivo o usa una imagen del sistema que incluya la versión 26.33.30 o una posterior.

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. Es posible que el editor de código muestre errores de referencia sin resolver en los archivos fuente del paquete de pruebas, incluso cuando el 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 se ejecutan solo 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 las copias de seguridad en los dispositivos que usan. Te recomendamos que uses dispositivos de prueba exclusivos.

Para conocer los cambios en cada versión, consulta las notas de la versión de prueba de copia de seguridad y restablecimiento.