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
BackupRestoreControllerpour 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
backupTestLibraryVersionpour définir la version desandroidx.test.backupbibliothè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
targetVariantspour spécifier les variantes de compilation que la suite teste, par exempletargetVariants.add("demoDebug"). Cette propriété est facultative. Si vous l'omettez, la suite teste les variantes dutestBuildTypede votre module, qui estdebugpar 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 toutapplicationIdSuffix.
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 :
- Insère chaque domaine de stockage.
- Arrête l'application.
- Sauvegarde l'application.
- Efface les données de l'application.
- Restaure la sauvegarde.
- 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 :
Preferenceprend le nom du fichierSharedPreferencessans l'extension.xml.TextFileetBinaryFilerésolvent les chemins relatifs par rapport aufilesDirde votre application.Databaseécrit la ligne avec SQL et crée la table avec des colonnesTEXTsi 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
phasesurBackupDeviceAction.PHASE_POPULATEpour une action qui insère des données, ou surBackupDeviceAction.PHASE_VERIFYpour une action qui vérifie les données. - Implémentez
executepour exécuter l'action avec leContextde votre application et les arguments du test hôte. Renvoie un résultat créé avec laBackupDeviceActionResult.successouBackupDeviceActionResult.failuremé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-5556Conserver 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:testDebugBackupTestDefaultTestSuiteLes 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.