Tests automatisés de sauvegarde et de restauration

Lorsque les utilisateurs configurent un nouvel appareil ou réinstallent votre application, Android peut restaurer les données de votre application à partir d'une sauvegarde dans le cloud ou d'un transfert d'appareil à appareil. Si vos règles de sauvegarde omettent des données importantes ou si votre application ne peut pas utiliser les données qu'elle restaure, les utilisateurs perdent leurs paramètres et leur progression sans voir d'erreur.

Les tests automatisés de sauvegarde et de restauration vous aident à détecter ces problèmes avant de publier votre application. Il s'agit de tests JUnit 5 qui s'exécutent sur votre ordinateur de développement et contrôlent les appareils connectés via Android Debug Bridge (adb). Chaque test insère des données d'application sur un appareil, sauvegarde et restaure l'application via le framework de sauvegarde Android, puis vérifie que les données sont intactes. Vous configurez les tests en tant que suite de tests du plug-in Android Gradle (AGP) test dans le fichier de compilation de votre module.

Fonctionnement

Une suite de tests de sauvegarde et de restauration comporte deux parties qui fonctionnent ensemble :

  • Les tests hôtes sont des tests JUnit 5 qui s'exécutent sur votre ordinateur de développement. Ils utilisent un BackupRestoreController pour chaque appareil afin d'insérer des données, de sauvegarder et de restaurer votre application, et de vérifier les résultats.
  • Les actions de l'appareil sont des classes qui implémentent BackupDeviceAction. Elles s'exécutent dans le processus de votre application sur l'appareil. Elles peuvent donc lire et écrire des données via le propre code de votre application. Les tests hôtes appellent les actions de l'appareil par nom de classe.

Lorsque vous exécutez la suite, AGP compile votre application et un APK de test contenant les actions de votre appareil. Avant chaque test, le framework de test installe les deux APK sur chaque appareil utilisé par le test, puis efface les données de votre application.

Types de stockage intégrés

Le tableau suivant répertorie les StorageDomain sous-classes qui vous permettent d'insérer et de vérifier des données sans écrire d'actions d'appareil :

Domaine de stockage Données
StorageDomain.Preference Clé dans un fichier SharedPreferences.
StorageDomain.Database Ligne dans une table de base de données SQLite.
StorageDomain.TextFile Fichier texte dans la mémoire de stockage interne de votre application.
StorageDomain.BinaryFile Fichier binaire dans la mémoire de stockage interne de votre application.

Pour tester d'autres données, telles que des fichiers DataStore, écrivez des actions d'appareil.

Modes de transport

Chaque sauvegarde utilise une BackupTransportMode valeur. Le tableau suivant répertorie les modes de transport compatibles :

Mode Scénario
LOCAL Sauvegarde dans le transport de sauvegarde local de l'appareil. Les données de sauvegarde restent sur l'appareil.
CLOUD_ENCRYPTED Sauvegarde dans le cloud, par exemple enregistrée dans le compte Google de l'utilisateur à partir d'un appareil avec chiffrement côté client.
CLOUD_UNENCRYPTED Sauvegarde dans le cloud à partir d'un appareil sans chiffrement côté client.
DEVICE_TO_DEVICE Transfert d'appareil à appareil, par exemple via un câble ou le Wi-Fi.

LOCAL fonctionne sur n'importe quel appareil compatible. Les modes cloud et d'appareil à appareil créent les mêmes sauvegardes que l'action Sauvegarder les données de l'application dans Android Studio : la sauvegarde est enregistrée sur votre ordinateur de développement au lieu d'être importée ou envoyée à un autre appareil. Les modes cloud et d'appareil à appareil nécessitent les services Google Play sur l'appareil et une version débogable de votre application. Si vos règles de sauvegarde dépendent du chiffrement côté client, testez les deux modes cloud.

Conditions requises

Pour utiliser les tests automatisés de sauvegarde et de restauration, assurez-vous que votre environnement répond aux exigences suivantes :

  • Gradle version 9.6.0 ou ultérieure.
  • JDK version 17 ou ultérieure.
  • Appareil ou émulateur exécutant Android 12 (niveau d'API 31) ou version ultérieure connecté via adb. Pour tester les transferts entre deux appareils, connectez-les.

Compatibilité

Le tableau suivant répertorie les versions minimales et recommandées pour les tests automatisés de sauvegarde et de restauration :

Composant Version minimale Version recommandée
Android Studio Rabbit 2 Canary 1 Rabbit 2 Canary 4 ou version ultérieure
Plug-in Android Gradle (AGP) 9.5.0-alpha06 9.5.0-alpha09 ou version ultérieure
Bibliothèques androidx.test.backup 1.0.0-alpha01 1.0.0-alpha02 ou version ultérieure

Chaque version preview d'AGP nécessite la version preview correspondante d'Android Studio. Pour en savoir plus, consultez les previews d'AGP.

Configurer votre projet

Pour ajouter une suite de tests de sauvegarde et de restauration au module de votre application, procédez comme suit :

Activer les indicateurs expérimentaux

Dans le fichier gradle.properties de votre projet, activez les suites de tests et les tests de sauvegarde :

android.experimental.testSuiteSupport=true
android.experimental.enableBackupTest=true

Déclarer la suite de tests

Dans le fichier build.gradle.kts du module de votre application, déclarez une suite de tests de sauvegarde dans le bloc testOptions :

android {
    testOptions {
        backupTests {
            create("backupTest")
        }
    }
}

Le nom de la suite, backupTest dans cet exemple, détermine les répertoires de l'ensemble de sources et les noms des tâches Gradle. Vous pouvez personnaliser la suite avec les propriétés facultatives suivantes :

  • Utilisez backupTestLibraryVersion pour définir la version des androidx.test.backup bibliothèques qu'AGP ajoute à la suite, par exemple "1.0.0-alpha02". Cette propriété est facultative. Si vous l'omettez, AGP revient à une version de bibliothèque par défaut en fonction de la version d'AGP.
  • Utilisez targetVariants pour spécifier les variantes de compilation que la suite teste, par exemple targetVariants.add("demoDebug"). Cette propriété est facultative. Si vous l'omettez, la suite teste les variantes du testBuildType de votre module, qui est debug par défaut. Nous vous recommandons de tester une variante débogable, car les modes de transport cloud et d'appareil à appareil nécessitent une application débogable.

AGP ajoute la bibliothèque hôte, androidx.test.backup:backup-host, et JUnit 5 aux tests hôtes. Il ajoute la bibliothèque d'appareil, androidx.test.backup:backup, à l'APK de test et définit l'exécuteur d'instrumentation de l'APK de test.

Pour ajouter d'autres dépendances, utilisez les blocs hostJar et testApk de la suite. Par exemple, pour utiliser Truth dans vos tests hôtes, ajoutez-le au hostJar bloc :

create("backupTest") {
    hostJar {
        dependencies {
            implementation("com.google.truth:truth:1.4.2")
        }
    }
}

Créer les ensembles de sources

Créez des répertoires d'ensemble de sources qui correspondent au nom de la suite :

  • Placez les tests hôtes dans src/{suiteName}/test/kotlin/.
  • Placez les actions de l'appareil dans src/{suiteName}/androidTest/kotlin/.

Par exemple, une suite nommée backupTest utilise la structure de répertoires suivante :

app/src/backupTest/
├── androidTest/
│   └── kotlin/com/example/myapp/ThemeActions.kt
└── test/
    └── kotlin/com/example/myapp/SettingsBackupTest.kt

Vérifier votre configuration de sauvegarde

Les tests sauvegardent et restaurent les données incluses dans la configuration de sauvegarde de votre application. Assurez-vous que la sauvegarde est activée pour votre application et que vos règles de sauvegarde incluent les données que vous testez.

Écrire des tests de sauvegarde et de restauration

Écrivez des tests hôtes dans l'ensemble de sources test de la suite. Chaque classe de test nécessite deux annotations :

  • @ExtendWith(BackupRestoreExtension::class) fournit des appareils connectés aux tests.
  • @BackupRestoreConfig(applicationId = "...") définit l'ID application de l'application testée. Utilisez l'ID application de la variante que vous testez, y compris tout applicationIdSuffix.

Chaque méthode de test déclare un paramètre BackupRestoreController pour chaque appareil qu'elle utilise.

Tester les types de stockage intégrés

Pour tester les types de stockage intégrés, appelez runBackupRestoreFlow avec les données à insérer, un répertoire pour le fichier de sauvegarde et un mode de transport. La méthode exécute les étapes suivantes sur l'appareil :

  1. Insère chaque domaine de stockage.
  2. Arrête l'application.
  3. Sauvegarde l'application.
  4. Efface les données de l'application.
  5. Restaure la sauvegarde.
  6. Vérifie que chaque domaine de stockage possède la valeur de graine.

Si une étape échoue, la méthode génère une exception, ce qui fait échouer le test. L'exemple suivant teste une clé SharedPreferences, une ligne SQLite et un fichier interne à l'aide du transport de sauvegarde 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,
        )
    }
}

Tenez compte des points suivants lorsque vous définissez des domaines de stockage :

  • Preference prend le nom du fichier SharedPreferences sans l'extension .xml.
  • TextFile et BinaryFile résolvent les chemins relatifs par rapport au filesDir de votre application.
  • Database écrit la ligne avec SQL et crée la table avec des colonnes TEXT si elle n'existe pas. Si votre application utilise Room, nous vous recommandons de tester le contenu de la base de données avec des actions d'appareil qui utilisent vos DAO afin que la base de données ait le schéma attendu par Room.

Écrire des actions d'appareil

Pour insérer ou vérifier des données que les types de stockage intégrés ne couvrent pas, écrivez des actions d'appareil dans l'ensemble de sources androidTest de la suite. Une action d'appareil implémente BackupDeviceAction et remplace deux membres :

  • Définissez phase sur BackupDeviceAction.PHASE_POPULATE pour une action qui insère des données, ou sur BackupDeviceAction.PHASE_VERIFY pour une action qui vérifie les données.
  • Implémentez execute pour exécuter l'action avec le Context de votre application et les arguments du test hôte. Renvoie un résultat créé avec la BackupDeviceActionResult.success ou BackupDeviceActionResult.failure méthode d’usine, et transmet un message d’erreur à failure.

La méthode execute s'exécute dans le processus de votre application sur un thread en arrière-plan. Elle peut donc se bloquer jusqu'à la fin des lectures et des écritures.

L'exemple suivant insère et vérifie un paramètre de thème que l'application stocke avec Preferences DataStore. L'application définit le DataStore dans son ensemble de sources 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")

Les actions de l'appareil utilisent les mêmes définitions :

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

Pour renvoyer des données au test hôte, transmettez une carte à la méthode success. Le test hôte lit la carte à partir de BackupActionResult.Success.data.

Exécuter chaque étape du flux

Pour utiliser des actions d'appareil ou personnaliser le flux, appelez chaque étape vous-même. Le test hôte suivant exécute les actions de l'appareil de la section précédente autour d'une sauvegarde et d'une restauration dans le cloud :

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

Lorsqu'une action d'appareil échoue, runOnDevice renvoie un BackupActionResult.Failure au lieu de générer une exception, vérifiez donc le résultat. Les autres étapes génèrent une exception en cas d'échec.

Tester les transferts entre appareils

Pour tester un transfert entre deux appareils, déclarez un paramètre BackupRestoreController pour chaque appareil. Sauvegardez l'application sur l'appareil source, puis restaurez le fichier de sauvegarde sur l'appareil cible. L'exemple suivant réutilise les constantes et l'assistant assertSuccess de l'exemple précédent :

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

La valeur role de @Device est un libellé qui facilite la lecture du test. Elle n'a aucune incidence sur l'appareil qu'un paramètre reçoit. Si un seul appareil est connecté, les deux paramètres utilisent cet appareil, et le test simule le transfert sur un seul appareil. L'effacement des données de la cible garantit que les données restaurées proviennent de la sauvegarde.

Sélectionner des appareils

Par défaut, le framework de test trie les appareils en ligne par numéro de série et les attribue aux paramètres BackupRestoreController par position. Par exemple, le premier paramètre de la méthode de test reçoit le premier appareil, et le deuxième paramètre reçoit le deuxième appareil. S'il n'y a pas d'appareil pour une position, le paramètre reçoit le premier appareil. Les positions comptent tous les paramètres de la méthode. Déclarez donc les paramètres de l'appareil avant les autres paramètres, tels que @TempDir. La variable d'environnement ANDROID_SERIAL n'a aucune incidence sur l'attribution des appareils.

Pour choisir des appareils, définissez les valeurs serial ou api de @Device, ou définissez les propriétés système de la JVM. Les propriétés système sont prioritaires par rapport aux valeurs @Device. Le tableau suivant répertorie les propriétés système compatibles :

Propriété système Description
androidx.test.backup.device.serial Numéro de série de l'appareil pour tous les paramètres de l'appareil.
androidx.test.backup.device.serials Liste de numéros de série séparés par une virgule, attribués aux paramètres par position.
androidx.test.backup.device.serial.{name} Numéro de série des paramètres annotés avec @Device(serial = "{name}").
androidx.test.backup.device.api Niveau d'API de l'appareil pour tous les paramètres de l'appareil.
androidx.test.backup.device.apis Liste de niveaux d'API séparés par une virgule, attribués aux paramètres par position.

Transmettez ces propriétés avec -D lorsque vous exécutez les tests à partir de la ligne de commande. Les propriétés s'appliquent à tous les tests exécutés. Utilisez donc l'option --tests pour n'exécuter que les tests qui en ont besoin. Par exemple, la commande suivante exécute ThemeTransferTest avec emulator-5554 comme appareil source et emulator-5556 comme appareil cible :

./gradlew :app:testDebugBackupTestDefaultTestSuite --tests "com.example.myapp.ThemeTransferTest" -Dandroidx.test.backup.device.serials=emulator-5554,emulator-5556

Conserver les données de l'application entre les tests

Avant chaque test, le framework de test efface les données de votre application sur chaque appareil utilisé par le test. Pour désactiver cette option pour une classe ou une méthode de test, annotez-la avec @Isolation(IsolationPolicy.MANUAL).

Écrire des tests en Java

Les méthodes BackupRestoreController sont des fonctions de suspension Kotlin. Si vous écrivez des tests hôtes en Java, utilisez la variante Async de chaque méthode, telle que runBackupRestoreFlowAsync, qui renvoie un ListenableFuture.

Exécuter des tests de sauvegarde et de restauration

AGP crée une tâche Gradle pour chaque variante testée par la suite, nommée test{Variant}{SuiteName}DefaultTestSuite. Pour exécuter la suite backupTest sur la variante debug, connectez vos appareils et exécutez la tâche :

./gradlew :app:testDebugBackupTestDefaultTestSuite

Les tâches check, test et connectedCheck n'exécutent pas les suites de tests de sauvegarde et de restauration. Exécutez donc explicitement la tâche de la suite.

Pour exécuter une seule classe ou méthode de test, utilisez l' --tests option :

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

Inspecter les rapports de test

Une fois l'exécution d'un test terminée, AGP génère un rapport de test HTML dans {module}/build/reports/tests/{taskName}/index.html, tel que app/build/reports/tests/testDebugBackupTestDefaultTestSuite/index.html.

Limites et problèmes connus

Les tests automatisés de sauvegarde et de restauration présentent les limites et problèmes connus suivants :

  • La compatibilité d'Android Studio avec les suites de tests n'est pas encore disponible. L'éditeur de code peut afficher des erreurs de référence non résolues dans les fichiers sources de la suite de tests, même si votre projet est correctement configuré. Compilez et exécutez plutôt des tests de sauvegarde et de restauration à partir de la ligne de commande à l'aide de Gradle.
  • Les tests ne s'exécutent que sur les appareils et les émulateurs connectés à votre ordinateur de développement via adb. Firebase Test Lab n'est pas compatible.
  • Les tests effacent les données de votre application et modifient les paramètres de sauvegarde sur les appareils qu'ils utilisent. Nous vous recommandons d'utiliser des appareils de test dédiés.

Pour connaître les modifications apportées à chaque version, consultez les notes de version des tests de sauvegarde et de restauration.