Teste automatizado de backup e restauração

Quando os usuários configuram um novo dispositivo ou reinstalam seu app, o Android pode restaurar os dados do app de um backup na nuvem ou de uma transferência de dispositivo para dispositivo. Se as regras de backup deixarem de fora dados importantes ou se o app não puder usar os dados restaurados, os usuários vão perder as configurações e o progresso sem receber uma mensagem de erro.

Os testes automatizados de backup e restauração ajudam a detectar esses problemas antes de lançar o app. Eles são testes JUnit 5 executados na sua máquina de desenvolvimento e controlam dispositivos conectados pelo Android Debug Bridge (adb). Cada teste inicializa os dados do app em um dispositivo, faz backup e restaura o app pelo framework de backup do Android e, em seguida, verifica se os dados estão intactos. Você configura os testes como um pacote de testes do Plug-in do Android para Gradle (AGP) test suite no arquivo de build do módulo.

Como funciona

Um pacote de testes de backup e restauração tem duas partes que funcionam juntas:

  • Testes de host são testes JUnit 5 executados na sua máquina de desenvolvimento. Eles usam um BackupRestoreController para cada dispositivo para inicializar dados, fazer backup e restaurar o app e verificar os resultados.
  • Ações do dispositivo são classes que implementam BackupDeviceAction. Elas são executadas no processo do app no dispositivo, para que possam ler e gravar dados pelo próprio código do app. Os testes de host chamam ações do dispositivo pelo nome da classe.

Quando você executa o pacote, o AGP cria o app e um APK de teste que contém as ações do dispositivo. Antes de cada teste, o framework de teste instala os dois APKs em cada dispositivo usado pelo teste e, em seguida, limpa os dados do app.

Tipos de armazenamento integrados

A tabela a seguir lista as StorageDomain subclasses que permitem inicializar e verificar dados sem gravar ações do dispositivo:

Domínio de armazenamento Dados
StorageDomain.Preference Uma chave em um arquivo SharedPreferences.
StorageDomain.Database Uma linha em uma tabela de banco de dados SQLite.
StorageDomain.TextFile Um arquivo de texto no armazenamento interno do app.
StorageDomain.BinaryFile Um arquivo binário no armazenamento interno do app.

Para testar outros dados, como arquivos do DataStore, grave ações do dispositivo.

Modos de transporte

Cada backup usa um BackupTransportMode valor. A tabela a seguir lista os modos de transporte com suporte:

Modo Cenário
LOCAL Um backup para o transporte de backup local do dispositivo. Os dados de backup permanecem no dispositivo.
CLOUD_ENCRYPTED Um backup na nuvem, como um salvo na Conta do Google do usuário em um dispositivo com criptografia do lado do cliente.
CLOUD_UNENCRYPTED Um backup na nuvem de um dispositivo que não tem criptografia do lado do cliente.
DEVICE_TO_DEVICE Uma transferência de dispositivo para dispositivo, por exemplo, por um cabo ou Wi-Fi.

LOCAL funciona em qualquer dispositivo com suporte. Os modos de nuvem e de dispositivo para dispositivo criam os mesmos backups que a ação Fazer backup dos dados do app no Android Studio: o backup é salvo na sua máquina de desenvolvimento em vez de ser enviado ou enviado para outro dispositivo. Os modos de nuvem e de dispositivo para dispositivo exigem o Google Play Services no dispositivo e um build depurável do app. Se as regras de backup dependem da criptografia do lado do cliente, teste os dois modos de nuvem.

Requisitos

Para usar testes automatizados de backup e restauração, verifique se o ambiente atende aos seguintes requisitos:

  • Gradle versão 9.6.0 ou mais recente.
  • JDK versão 17 ou mais recente.
  • Um dispositivo ou emulador com o Android 12 (nível 31 da API) ou mais recente conectado pelo adb. Para testar transferências entre dois dispositivos, conecte dois dispositivos.

Compatibilidade

A tabela a seguir lista as versões mínima e recomendada para testes automatizados de backup e restauração:

Componente Versão mínima Versão recomendada
Android Studio Rabbit 2 Canary 1 Rabbit 2 Canary 4 ou mais recente
Plug-in do Android para Gradle (AGP) 9.5.0-alpha06 9.5.0-alpha09 ou mais recente
Bibliotecas androidx.test.backup 1.0.0-alpha01 1.0.0-alpha02 ou mais recente

Cada versão de pré-lançamento do AGP exige a versão de pré-lançamento correspondente do Android Studio. Para mais informações, consulte Pré-visualizações do AGP.

Criar o projeto

Para adicionar um pacote de testes de backup e restauração ao módulo do app, siga estas etapas:

Ativar flags experimentais

No arquivo gradle.properties do projeto, ative os pacotes de testes e os testes de backup:

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

Declarar o pacote de testes

No arquivo build.gradle.kts do módulo do app, declare um pacote de testes de backup no bloco testOptions:

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

O nome do pacote, backupTest neste exemplo, determina os diretórios do conjunto de origem e os nomes das tarefas do Gradle. Você pode personalizar o pacote com as seguintes propriedades opcionais:

  • Use backupTestLibraryVersion para definir a versão das androidx.test.backup bibliotecas que o AGP adiciona ao pacote, como "1.0.0-alpha02". Esta propriedade é opcional. Se você omitir, o AGP vai usar uma versão padrão da biblioteca com base na versão do AGP.
  • Use targetVariants para especificar as variantes de build que o pacote testa, como targetVariants.add("demoDebug"). Esta propriedade é opcional. Se você omitir, o pacote vai testar as variantes de testBuildType do módulo, que é debug por padrão. Recomendamos testar uma variante depurável porque os modos de transporte de nuvem e de dispositivo para dispositivo exigem um app depurável.

O AGP adiciona a biblioteca de host, androidx.test.backup:backup-host, e o JUnit 5 aos testes de host. Ele adiciona a biblioteca de dispositivos, androidx.test.backup:backup, ao APK de teste e define o runner de instrumentação do APK de teste.

Para adicionar outras dependências, use os blocos hostJar e testApk do pacote. Por exemplo, para usar Truth nos testes de host, adicione-o ao hostJar bloco:

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

Criar os conjuntos de origem

Crie diretórios de conjunto de origem que correspondam ao nome do pacote:

  • Coloque testes de host em src/{suiteName}/test/kotlin/.
  • Coloque ações do dispositivo em src/{suiteName}/androidTest/kotlin/.

Por exemplo, um pacote chamado backupTest usa a seguinte estrutura de diretórios:

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

Verificar a configuração de backup

Os testes fazem backup e restauram os dados incluídos na configuração de backup do app. Verifique se o backup está ativado para o app e se as regras de backup incluem os dados que você testa.

Escrever testes de backup e restauração

Escreva testes de host no conjunto de origem test do pacote. Cada classe de teste precisa de duas anotações:

  • @ExtendWith(BackupRestoreExtension::class) fornece dispositivos conectados aos testes.
  • @BackupRestoreConfig(applicationId = "...") define o ID do aplicativo de o app em teste. Use o ID do aplicativo da variante que você testa, incluindo qualquer applicationIdSuffix.

Cada método de teste declara um parâmetro BackupRestoreController para cada dispositivo usado.

Testar tipos de armazenamento integrados

Para testar tipos de armazenamento integrados, chame runBackupRestoreFlow com os dados a serem inicializados, um diretório para o arquivo de backup e um modo de transporte. O método executa as seguintes etapas no dispositivo:

  1. Inicializa cada domínio de armazenamento.
  2. Interrompe o app.
  3. Faz backup do app.
  4. Limpa os dados do app.
  5. Restaura o backup.
  6. Verifica se cada domínio de armazenamento tem o valor de semente.

Se uma etapa falhar, o método vai gerar uma exceção, o que vai fazer com que o teste falhe. O exemplo a seguir testa uma chave SharedPreferences, uma linha SQLite e um arquivo interno usando o transporte de backup 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,
        )
    }
}

Lembre-se do seguinte ao definir domínios de armazenamento:

  • Preference usa o nome do arquivo SharedPreferences sem a .xml extensão.
  • TextFile e BinaryFile resolvem caminhos relativos em relação ao filesDir do app.
  • Database grava a linha com SQL e cria a tabela com colunas TEXT se ela não existir. Se o app usa o Room, recomendamos testar o conteúdo do banco de dados com ações do dispositivo que usam seus DAOs para que o banco de dados tenha o esquema esperado pelo Room.

Escrever ações do dispositivo

Para inicializar ou verificar dados que os tipos de armazenamento integrados não cobrem, grave ações do dispositivo no conjunto de origem androidTest do pacote. Uma ação do dispositivo implementa BackupDeviceAction e substitui dois membros:

  • Defina phase como BackupDeviceAction.PHASE_POPULATE para uma ação que inicializa dados ou BackupDeviceAction.PHASE_VERIFY para uma ação que verifica dados.
  • Implemente execute para executar a ação com o Context do app e os argumentos do teste de host. Retorne um resultado criado com o BackupDeviceActionResult.success ou BackupDeviceActionResult.failure método de fábrica e transmita uma mensagem de erro para failure.

O método execute é executado no processo do app em uma linha de execução em segundo plano, para que ele possa ser bloqueado até que as leituras e gravações sejam concluídas.

O exemplo a seguir inicializa e verifica uma configuração de tema que o app armazena com Preferences DataStore. O app define o DataStore no conjunto de origem 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")

As ações do dispositivo usam as mesmas definições:

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

import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.test.backup.BackupDeviceAction
import androidx.test.backup.BackupDeviceActionArgs
import androidx.test.backup.BackupDeviceActionResult
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking

class SeedThemeAction : BackupDeviceAction {
    override val phase = BackupDeviceAction.PHASE_POPULATE

    override fun execute(
        context: Context,
        args: BackupDeviceActionArgs,
    ): BackupDeviceActionResult {
        val theme = args["theme"]
            ?: return BackupDeviceActionResult.failure("Missing theme argument")
        runBlocking {
            context.settingsDataStore.edit { it[THEME_KEY] = theme }
        }
        return BackupDeviceActionResult.success()
    }
}

class VerifyThemeAction : BackupDeviceAction {
    override val phase = BackupDeviceAction.PHASE_VERIFY

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

Para retornar dados ao teste de host, transmita um mapa para o método success. O teste de host lê o mapa de BackupActionResult.Success.data.

Executar cada etapa do fluxo

Para usar ações do dispositivo ou personalizar o fluxo, chame cada etapa. O teste de host a seguir executa as ações do dispositivo da seção anterior em torno de um backup e restauração na nuvem:

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 uma ação do dispositivo falha, runOnDevice retorna um BackupActionResult.Failure em vez de gerar uma exceção, portanto, verifique o resultado. As outras etapas geram uma exceção se falharem.

Testar transferências entre dispositivos

Para testar uma transferência entre dois dispositivos, declare um parâmetro BackupRestoreController para cada dispositivo. Faça backup do app no dispositivo de origem e restaure o arquivo de backup no dispositivo de destino. O exemplo a seguir reutiliza as constantes e o auxiliar assertSuccess do exemplo anterior:

package com.example.myapp

import androidx.test.backup.host.BackupRestoreConfig
import androidx.test.backup.host.BackupRestoreController
import androidx.test.backup.host.BackupRestoreExtension
import androidx.test.backup.host.BackupTransportMode
import androidx.test.backup.host.Device
import java.nio.file.Path
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.io.TempDir

@ExtendWith(BackupRestoreExtension::class)
@BackupRestoreConfig(applicationId = "com.example.myapp")
class ThemeTransferTest {

    @Test
    fun themeTransfersToNewDevice(
        @Device(role = "source") source: BackupRestoreController,
        @Device(role = "target") target: BackupRestoreController,
        @TempDir outputDir: Path,
    ): Unit = runBlocking {
        val args = mapOf("theme" to "dark")
        source.runOnDevice(SEED_THEME, args).assertSuccess()
        source.stopApp()
        val backupFile = source.performBackup(
            BackupTransportMode.DEVICE_TO_DEVICE,
            outputDir,
        )

        target.clearAppData()
        target.performRestore(backupFile)
        target.runOnDevice(VERIFY_THEME, args).assertSuccess()
    }
}

O valor role de @Device é um rótulo que facilita a leitura do teste. Ele não afeta qual dispositivo um parâmetro recebe. Se apenas um dispositivo estiver conectado, os dois parâmetros vão usar esse dispositivo, e o teste vai simular a transferência em um único dispositivo. Limpar os dados do destino garante que os dados restaurados venham do backup.

Escolher dispositivos

Por padrão, o framework de teste classifica os dispositivos on-line pelo número de série e os atribui aos parâmetros BackupRestoreController por posição. Por exemplo, o primeiro parâmetro do método de teste recebe o primeiro dispositivo, e o segundo parâmetro recebe o segundo dispositivo. Se não houver um dispositivo para uma posição, o parâmetro vai receber o primeiro dispositivo. As posições contam todos os parâmetros do método. Portanto, declare os parâmetros do dispositivo antes de outros parâmetros, como @TempDir. A variável de ambiente ANDROID_SERIAL não afeta a atribuição de dispositivos.

Para escolher dispositivos, defina os valores serial ou api de @Device ou defina as propriedades do sistema JVM. As propriedades do sistema têm precedência sobre os valores @Device. A tabela a seguir lista as propriedades do sistema com suporte:

Propriedade do sistema Descrição
androidx.test.backup.device.serial O número de série do dispositivo para todos os parâmetros do dispositivo.
androidx.test.backup.device.serials Uma lista separada por vírgulas de números de série, atribuídos a parâmetros por posição.
androidx.test.backup.device.serial.{name} O número de série dos parâmetros anotados com @Device(serial = "{name}").
androidx.test.backup.device.api O nível da API do dispositivo para todos os parâmetros do dispositivo.
androidx.test.backup.device.apis Uma lista separada por vírgulas de níveis da API, atribuídos a parâmetros por posição.

Transmita essas propriedades com -D ao executar os testes na linha de comando. As propriedades se aplicam a todos os testes executados. Portanto, use a opção --tests para executar apenas os testes que precisam delas. Por exemplo, o comando a seguir executa ThemeTransferTest com emulator-5554 como o dispositivo de origem e emulator-5556 como o dispositivo de destino:

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

Manter os dados do app entre os testes

Antes de cada teste, o framework de teste limpa os dados do app em cada dispositivo usado pelo teste. Para desativar isso para uma classe ou método de teste, anote-o com @Isolation(IsolationPolicy.MANUAL).

Escrever testes em Java

Os métodos BackupRestoreController são funções de suspensão do Kotlin. Se você escrever testes de host em Java, use a variante Async de cada método, como runBackupRestoreFlowAsync, que retorna um ListenableFuture.

Executar testes de backup e restauração

O AGP cria uma tarefa do Gradle para cada variante que o pacote testa, chamada test{Variant}{SuiteName}DefaultTestSuite. Para executar o pacote backupTest na variante debug, conecte os dispositivos e execute a tarefa:

./gradlew :app:testDebugBackupTestDefaultTestSuite

As tarefas check, test e connectedCheck não executam pacotes de testes de backup e restauração. Portanto, execute a tarefa do pacote explicitamente.

Para executar uma única classe ou método de teste, use a --tests opção:

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

Inspecionar relatórios de teste

Depois que uma execução de teste é concluída, o AGP gera um relatório de teste HTML em {module}/build/reports/tests/{taskName}/index.html, como app/build/reports/tests/testDebugBackupTestDefaultTestSuite/index.html.

Limitações e problemas conhecidos

Os testes automatizados de backup e restauração têm os seguintes problemas e limitações conhecidos:

  • O suporte do Android Studio para pacotes de testes ainda não está disponível. O editor de código pode mostrar erros de referência não resolvidos em arquivos de origem do pacote de testes, mesmo quando o projeto está configurado corretamente. Crie e execute testes de backup e restauração na linha de comando usando o Gradle.
  • Os testes são executados apenas em dispositivos e emuladores conectados à sua máquina de desenvolvimento pelo adb. O Firebase Test Lab não tem suporte.
  • Os testes limpam os dados do app e mudam as configurações de backup nos dispositivos usados. Recomendamos o uso de dispositivos de teste dedicados.

Para mudanças em cada versão, consulte as notas da versão de testes de backup e restauração.