Test automatizzati di backup e ripristino

Quando gli utenti configurano un nuovo dispositivo o reinstallano la tua app, Android può ripristinare i dati dell'app da un backup sul cloud o da un trasferimento da dispositivo a dispositivo. Se le regole di backup escludono dati importanti o se l'app non riesce a utilizzare i dati che ripristina, gli utenti perdono le impostazioni e i progressi senza visualizzare un errore.

I test di backup e ripristino automatici ti aiutano a rilevare questi problemi prima di rilasciare l'app. Si tratta di test JUnit 5 che vengono eseguiti sulla macchina di sviluppo e controllano i dispositivi connessi tramite Android Debug Bridge (adb). Ogni test inserisce i dati dell'app su un dispositivo, esegue il backup e il ripristino dell'app tramite il framework di backup di Android e poi verifica che i dati siano intatti. Configura i test come una suite di test del plug-in Android per Gradle (AGP) nel file di build del modulo.

Come funziona

Una suite di test di backup e ripristino è composta da due parti che funzionano insieme:

  • I test host sono test JUnit 5 che vengono eseguiti sulla macchina di sviluppo. Utilizzano un BackupRestoreController per ogni dispositivo per inserire i dati, eseguire il backup e il ripristino dell'app e controllare i risultati.
  • Le azioni dispositivo sono classi che implementano BackupDeviceAction. Vengono eseguite nel processo dell'app sul dispositivo, quindi possono leggere e scrivere dati tramite il codice dell'app. I test host chiamano le azioni dispositivo per nome della classe.

Quando esegui la suite, AGP crea la tua app e un APK di test che contiene le azioni dispositivo. Prima di ogni test, il framework di test installa entrambi gli APK su ogni dispositivo utilizzato dal test, quindi cancella i dati dell'app.

Tipi di archiviazione integrati

La tabella seguente elenca le StorageDomain sottoclassi che consentono di inserire e verificare i dati senza scrivere azioni dispositivo:

Dominio di archiviazione Dati
StorageDomain.Preference Una chiave in un file SharedPreferences.
StorageDomain.Database Una riga in una tabella di database SQLite.
StorageDomain.TextFile Un file di testo nella memoria interna dell'app.
StorageDomain.BinaryFile Un file binario nella memoria interna dell'app.

Per testare altri dati, ad esempio i file DataStore, scrivi azioni dispositivo.

Modalità di trasporto

Ogni backup utilizza un BackupTransportMode valore. La tabella seguente elenca le modalità di trasporto supportate:

Modalità Scenario
LOCAL Un backup nel trasporto di backup locale del dispositivo. I dati di backup rimangono sul dispositivo.
CLOUD_ENCRYPTED Un backup sul cloud, ad esempio uno salvato nell'Account Google dell'utente da un dispositivo con crittografia lato client.
CLOUD_UNENCRYPTED Un backup sul cloud da un dispositivo che non ha la crittografia lato client.
DEVICE_TO_DEVICE Un trasferimento da dispositivo a dispositivo, ad esempio tramite cavo o Wi-Fi.

LOCAL funziona su qualsiasi dispositivo supportato. Le modalità cloud e da dispositivo a dispositivo creano gli stessi backup dell'azione Esegui backup dei dati dell'app in Android Studio: il backup viene salvato sulla macchina di sviluppo anziché essere caricato o inviato a un altro dispositivo. Le modalità cloud e da dispositivo a dispositivo richiedono i servizi Google Play sul dispositivo e una build di debug dell'app. Se le regole di backup dipendono dalla crittografia lato client, testa entrambe le modalità cloud.

Requisiti

Per utilizzare i test di backup e ripristino automatici, assicurati che il tuo ambiente soddisfi i seguenti requisiti:

  • Gradle versione 9.6.0 o successive.
  • JDK versione 17 o successive.
  • Un dispositivo o un emulatore con Android 12 (livello API 31) o versioni successive connesso tramite adb. Per testare i trasferimenti tra due dispositivi, connettili entrambi.

Compatibilità

La tabella seguente elenca le versioni minime e consigliate per i test di backup e ripristino automatici:

Componente Versione minima Versione consigliata
Android Studio Rabbit 2 Canary 1 Rabbit 2 Canary 4 o versioni successive
Plug-in Android per Gradle (AGP) 9.5.0-alpha06 9.5.0-alpha09 o versioni successive
Librerie androidx.test.backup 1.0.0-alpha01 1.0.0-alpha02 o versioni successive

Ogni versione di anteprima di AGP richiede la versione di anteprima corrispondente di Android Studio. Per saperne di più, consulta Anteprime di AGP.

Configura il progetto

Per aggiungere una suite di test di backup e ripristino al modulo dell'app, completa i seguenti passaggi:

Attiva i flag sperimentali

Nel file gradle.properties del progetto, attiva le suite di test e i test di backup:

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

Dichiara la suite di test

Nel file build.gradle.kts del modulo dell'app, dichiara una suite di test di backup nel blocco testOptions:

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

Il nome della suite, backupTest in questo esempio, determina le directory del set di risorse e i nomi delle attività Gradle. Puoi personalizzare la suite con le seguenti proprietà facoltative:

  • Utilizza backupTestLibraryVersion per impostare la versione delle androidx.test.backup librerie che AGP aggiunge alla suite, ad esempio "1.0.0-alpha02". Questa proprietà è facoltativa. Se la ometti, AGP utilizza una versione predefinita della libreria in base alla versione di AGP.
  • Utilizza targetVariants per specificare le varianti di compilazione testate dalla suite, ad esempio targetVariants.add("demoDebug"). Questa proprietà è facoltativa. Se la ometti, la suite testa le varianti di testBuildType del modulo, che per impostazione predefinita è debug. Ti consigliamo di testare una variante di debug perché le modalità di trasporto cloud e da dispositivo a dispositivo richiedono un'app di cui è possibile eseguire il debug.

AGP aggiunge la libreria host, androidx.test.backup:backup-host, e JUnit 5 ai test host. Aggiunge la libreria del dispositivo, androidx.test.backup:backup, all'APK di test e imposta il runner di instrumentazione dell'APK di test.

Per aggiungere altre dipendenze, utilizza i blocchi hostJar e testApk della suite. Ad esempio, per utilizzare Truth nei test host, aggiungilo al hostJar blocco:

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

Crea i set di risorse

Crea directory del set di risorse che corrispondano al nome della suite:

  • Inserisci i test host in src/{suiteName}/test/kotlin/.
  • Inserisci le azioni dispositivo in src/{suiteName}/androidTest/kotlin/.

Ad esempio, una suite denominata backupTest utilizza la seguente struttura di directory:

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

Controlla la configurazione di backup

I test eseguono il backup e il ripristino dei dati inclusi nella configurazione di backup dell'app. Assicurati che il backup sia attivato per l'app e che le regole di backup includano i dati che stai testando.

Scrivi test di backup e ripristino

Scrivi i test host nel set di risorse test della suite. Ogni classe di test richiede due annotazioni:

  • @ExtendWith(BackupRestoreExtension::class) fornisce i dispositivi connessi ai test.
  • @BackupRestoreConfig(applicationId = "...") imposta l'ID applicazione di l'app in fase di test. Utilizza l'ID applicazione della variante che stai testando, incluso qualsiasi applicationIdSuffix.

Ogni metodo di test dichiara un parametro BackupRestoreController per ogni dispositivo che utilizza.

Testa i tipi di archiviazione integrati

Per testare i tipi di archiviazione integrati, chiama runBackupRestoreFlow con i dati da inserire, una directory per il file di backup e una modalità di trasporto. Il metodo esegue i seguenti passaggi sul dispositivo:

  1. Inserisce ogni dominio di archiviazione.
  2. Arresta l'app.
  3. Esegue il backup dell'app.
  4. Cancella i dati dell'app.
  5. Ripristina il backup.
  6. Verifica che ogni dominio di archiviazione abbia il valore inserito.

Se un passaggio non riesce, il metodo genera un'eccezione, che fa fallire il test. L'esempio seguente testa una chiave SharedPreferences, una riga SQLite e un file interno utilizzando il trasporto di backup locale:

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

Quando definisci i domini di archiviazione, tieni presente quanto segue:

  • Preference accetta il nome del file SharedPreferences senza l'estensione .xml.
  • TextFile e BinaryFile risolvono i percorsi relativi rispetto a filesDir dell'app.
  • Database scrive la riga con SQL e crea la tabella con le colonne TEXT se la tabella non esiste. Se l'app utilizza Room, ti consigliamo di testare i contenuti del database con le azioni dispositivo che utilizzano i DAO in modo che il database abbia lo schema previsto da Room.

Scrivi azioni dispositivo

Per inserire o verificare i dati non coperti dai tipi di archiviazione integrati, scrivi azioni dispositivo nel set di origine androidTest della suite. Un'azione dispositivo implementa BackupDeviceAction e sostituisce due membri:

  • Imposta phase su BackupDeviceAction.PHASE_POPULATE per un'azione che inserisce i dati o su BackupDeviceAction.PHASE_VERIFY per un'azione che controlla i dati.
  • Implementa execute per eseguire l'azione con Context dell'app e gli argomenti del test host. Restituisci un risultato creato con il BackupDeviceActionResult.success o BackupDeviceActionResult.failure metodo factory e passa un messaggio di errore a failure.

Il metodo execute viene eseguito nel processo dell'app su un thread in background, quindi può essere bloccato fino al completamento delle operazioni di lettura e scrittura.

L'esempio seguente inserisce e verifica un'impostazione del tema che l'app memorizza con Preferences DataStore. L'app definisce DataStore nel set di risorse principale:

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

Le azioni dispositivo utilizzano le stesse definizioni:

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

Per restituire i dati al test host, passa una mappa al metodo success. Il test host legge la mappa da BackupActionResult.Success.data.

Esegui ogni passaggio del flusso

Per utilizzare le azioni dispositivo o personalizzare il flusso, chiama ogni passaggio. Il seguente test host esegue le azioni dispositivo della sezione precedente intorno a un backup e ripristino sul 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)
}

Quando un'azione dispositivo non riesce, runOnDevice restituisce un BackupActionResult.Failure anziché generare un'eccezione, quindi controlla il risultato. Gli altri passaggi generano un'eccezione se non riescono.

Testa i trasferimenti tra dispositivi

Per testare un trasferimento tra due dispositivi, dichiara un parametro BackupRestoreController per ogni dispositivo. Esegui il backup dell'app sul dispositivo di origine, quindi ripristina il file di backup sul dispositivo di destinazione. L'esempio seguente riutilizza le costanti e l'helper assertSuccess dell'esempio precedente:

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

Il valore role di @Device è un'etichetta che semplifica la lettura del test. Non influisce sul dispositivo a cui viene assegnato un parametro. Se è connesso un solo dispositivo, entrambi i parametri utilizzano quel dispositivo e il test simula il trasferimento su un singolo dispositivo. La cancellazione dei dati della destinazione garantisce che i dati ripristinati provengano dal backup.

Scegli dispositivi

Per impostazione predefinita, il framework di test ordina i dispositivi online in base al numero di serie e li assegna ai parametri BackupRestoreController in base alla posizione. Ad esempio, il primo parametro del metodo di test riceve il primo dispositivo e il secondo parametro riceve il secondo dispositivo. Se non è presente alcun dispositivo per una posizione, il parametro riceve il primo dispositivo. Le posizioni contano tutti i parametri del metodo, quindi dichiara i parametri del dispositivo prima di altri parametri, ad esempio @TempDir. La variabile di ambiente ANDROID_SERIAL non influisce sull'assegnazione dei dispositivi.

Per scegliere i dispositivi, imposta i valori serial o api di @Device oppure imposta le proprietà di sistema JVM. Le proprietà di sistema hanno la precedenza sui valori @Device. La tabella seguente elenca le proprietà di sistema supportate:

Proprietà di sistema Descrizione
androidx.test.backup.device.serial Il numero di serie del dispositivo per tutti i parametri del dispositivo.
androidx.test.backup.device.serials Un elenco separato da virgole di numeri di serie, assegnati ai parametri in base alla posizione.
androidx.test.backup.device.serial.{name} Il numero di serie per i parametri annotati con @Device(serial = "{name}").
androidx.test.backup.device.api Il livello API del dispositivo per tutti i parametri del dispositivo.
androidx.test.backup.device.apis Un elenco di livelli API separati da virgole, assegnati ai parametri in base alla posizione.

Passa queste proprietà con -D quando esegui i test dalla riga di comando. Le proprietà si applicano a ogni test eseguito, quindi utilizza l'opzione --tests per eseguire solo i test che le richiedono. Ad esempio, il seguente comando esegue ThemeTransferTest con emulator-5554 come dispositivo di origine e emulator-5556 come dispositivo di destinazione:

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

Conserva i dati dell'app tra i test

Prima di ogni test, il framework di test cancella i dati dell'app su ogni dispositivo utilizzato dal test. Per disattivare questa opzione per una classe o un metodo di test, annotalo con @Isolation(IsolationPolicy.MANUAL).

Scrivi test in Java

I metodi BackupRestoreController sono funzioni di sospensione Kotlin. Se scrivi test host in Java, utilizza la variante Async di ogni metodo, ad esempio runBackupRestoreFlowAsync, che restituisce un ListenableFuture.

Esegui test di backup e ripristino

AGP crea un'attività Gradle per ogni variante testata dalla suite, denominata test{Variant}{SuiteName}DefaultTestSuite. Per eseguire la suite backupTest sulla variante debug, connetti i dispositivi ed esegui l'attività:

./gradlew :app:testDebugBackupTestDefaultTestSuite

Le attività check, test e connectedCheck non eseguono le suite di test di backup e ripristino, quindi esegui esplicitamente l'attività della suite.

Per eseguire una singola classe o un singolo metodo di test, utilizza l' --testsopzione:

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

Esamina i report di test

Al termine di un'esecuzione di test, AGP genera un report di test HTML in {module}/build/reports/tests/{taskName}/index.html, ad esempio app/build/reports/tests/testDebugBackupTestDefaultTestSuite/index.html.

Problemi noti e limitazioni

I test di backup e ripristino automatici presentano i seguenti problemi noti e limitazioni:

  • Il supporto di Android Studio per le suite di test non è ancora disponibile. L'editor di codice potrebbe mostrare errori di riferimento non risolti nei file di origine della suite di test anche se il progetto è configurato correttamente. Crea ed esegui i test di backup e ripristino dalla riga di comando utilizzando Gradle.
  • I test vengono eseguiti solo su dispositivi ed emulatori connessi alla macchina di sviluppo tramite adb. Firebase Test Lab non è supportato.
  • I test cancellano i dati dell'app e modificano le impostazioni di backup sui dispositivi che utilizzano. Ti consigliamo di utilizzare dispositivi di test dedicati.

Per le modifiche in ogni release, consulta le note di rilascio dei test di backup e ripristino.