Автоматическое тестирование резервного копирования и восстановления

Когда пользователи настраивают новое устройство или переустанавливают приложение, Android может восстановить данные приложения из облачной резервной копии или путем переноса с одного устройства на другое. Если правила резервного копирования не учитывают важные данные или приложение не может использовать восстановленные данные, пользователи потеряют свои настройки и прогресс, не увидев ошибки.

Автоматизированные тесты резервного копирования и восстановления помогают выявить эти проблемы до выпуска приложения. Это тесты JUnit 5, которые выполняются на компьютере для разработки и управляют подключенными устройствами через Android Debug Bridge (adb). Каждый тест заполняет устройство данными приложения, создает резервную копию и восстанавливает приложение с помощью фреймворка резервного копирования Android, а затем проверяет, не повреждены ли данные. Тесты настраиваются как набор тестов плагина Android Gradle (AGP) в файле сборки модуля.

Принцип работы

Набор тестов для резервного копирования и восстановления состоит из двух частей, которые работают вместе:

  • Тесты хоста – это тесты JUnit 5, которые выполняются на компьютере для разработки. Для каждого устройства они используют BackupRestoreController, чтобы заполнить данные, создать резервную копию приложения, восстановить его и проверить результаты.
  • Действия на устройствах – это классы, которые реализуют BackupDeviceAction. Они выполняются в процессе приложения на устройстве, поэтому могут читать и записывать данные через собственный код приложения. Тесты хоста вызывают действия устройства по имени класса.

Когда вы запускаете набор, AGP создает приложение и тестовый APK-файл, содержащий действия на устройстве. Фреймворк устанавливает оба APK-файла на каждое устройство, используемое для тестирования, и очищает данные приложения перед каждым тестом.

Типы встроенных хранилищ

В таблице ниже перечислены подклассы StorageDomain, которые позволяют заполнять и проверять данные без написания действий на устройстве.

Домен хранилища Данные
StorageDomain.Preference Ключ в файле SharedPreferences.
StorageDomain.DataStorePreference Ключ в файле Preferences DataStore.
StorageDomain.Database Строка в таблице базы данных SQLite.
StorageDomain.TextFile Текстовый файл во внутреннем хранилище приложения.
StorageDomain.BinaryFile Двоичный файл во внутреннем хранилище приложения.
StorageDomain.ExcludedData Другой встроенный домен хранилища, данные которого не должны восстанавливаться, например данные, исключенные правилами резервного копирования.

Чтобы протестировать другие данные, например файлы Proto DataStore или базы данных Room, напишите действия устройства.

Способы передвижения

Для каждой резервной копии используется значение BackupTransportMode. В таблице ниже перечислены поддерживаемые способы передвижения.

Режим Сценарий
LOCAL Резервное копирование в локальное хранилище устройства. Резервные копии данных остаются на устройстве.
CLOUD_ENCRYPTED Облачная резервная копия, например сохраненная в аккаунте Google пользователя с устройства с клиентским шифрованием.
CLOUD_UNENCRYPTED Резервная копия в облаке с устройства, на котором не используется шифрование на стороне клиента.
DEVICE_TO_DEVICE Перенос данных с одного устройства на другое, например по кабелю или Wi-Fi.

LOCAL работает на любом поддерживаемом устройстве. Режимы облака и передачи между устройствами создают такие же резервные копии, как и действие Резервное копирование данных приложения в Android Studio. Резервная копия сохраняется на компьютере для разработки, а не загружается или отправляется на другое устройство. Для режимов облака и передачи данных между устройствами требуются сервисы Google Play на устройстве и отладочная сборка приложения. Если правила резервного копирования зависят от клиентского шифрования, протестируйте оба режима облака.

Требования

Чтобы использовать автоматическое резервное копирование и тестирование восстановления, убедитесь, что ваша среда соответствует следующим требованиям:

  • Gradle версии 9.6.0 или более поздней.
  • JDK версии 17 или более поздней.
  • Устройство или эмулятор с Android 12 (уровень API 31) или более поздней версии, подключенные через adb:
    • Для устройств и эмуляторов с уровнем API 36.1 или выше требуются сервисы Google Play версии 26.33.30 или выше.
    • Чтобы протестировать передачу данных между двумя устройствами, подключите их.

Совместимость

В таблице ниже указаны минимальные и рекомендуемые версии для автоматического тестирования резервного копирования и восстановления.

Компонент Минимальная версия Рекомендуемая версия
Android Studio Кролик 2, канарейка 1 Rabbit 2 Canary 4 или более поздней версии
Плагин Android Gradle (AGP) 9.5.0-alpha06 9.5.0-alpha09 или более поздней версии
Библиотек: androidx.test.backup 1.0.0-alpha01 1.0.0-alpha04 или более поздней версии

Для каждой предварительной версии AGP требуется соответствующая предварительная версия Android Studio. Подробнее о предварительных версиях плагина Android Gradle…

Настройте проект

Чтобы добавить набор тестов для резервного копирования и восстановления в модуль приложения, выполните следующие действия:

Включить экспериментальные флаги

В файле gradle.properties проекта включите наборы тестов и резервные тесты:

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

Объявление набора тестов

В файле build.gradle.kts модуля приложения объявите набор тестов резервного копирования в блоке testOptions:

android {
    testOptions {
        backupTests {
            create("backupTest") {
                backupTestLibraryVersion = "1.0.0-alpha04"
            }
        }
    }
}

Название набора (в данном примере – backupTest) определяет каталоги наборов исходных файлов и названия задач Gradle. Вы можете настроить набор с помощью следующих необязательных свойств:

  • Используйте backupTestLibraryVersion, чтобы задать версию библиотек androidx.test.backup, которые AGP добавляет в набор, например "1.0.0-alpha04". Это необязательное свойство. Если вы не укажете версию, AGP будет использовать версию библиотеки по умолчанию, которая зависит от версии AGP. Для примеров на этой странице требуется версия 1.0.0-alpha04 или выше.
  • Используйте targetVariants, чтобы указать варианты сборки, которые тестирует набор, например targetVariants.add("demoDebug"). Это необязательное свойство. Если вы не укажете его, набор будет тестировать варианты testBuildType вашего модуля, который по умолчанию имеет значение debug. Рекомендуем тестировать отладочный вариант, поскольку для облачного режима и режима передачи между устройствами требуется отладочное приложение.

AGP добавляет в тесты хоста библиотеку хоста, androidx.test.backup:backup-host и JUnit 5. Она добавляет библиотеку устройств androidx.test.backup:backup в тестовый APK-файл и задает для него средство запуска инструментирования.

Чтобы добавить другие зависимости, используйте блоки hostJar и testApk. Например, чтобы использовать Truth в тестах хоста, добавьте его в блок hostJar:

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

Создайте наборы исходных файлов

Создайте каталоги наборов исходных файлов, соответствующие названию набора:

  • Поместите тесты хоста в src/{suiteName}/test/kotlin/.
  • Поместите действия на устройствах в src/{suiteName}/androidTest/kotlin/.

Например, для набора backupTest используется следующая структура каталогов:

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

Проверьте настройки резервного копирования

Тесты создают резервную копию и восстанавливают данные, которые включены в конфигурацию резервного копирования приложения. Убедитесь, что для приложения включено резервное копирование и что правила резервного копирования включают данные, которые вы тестируете.

Как написать тесты для резервного копирования и восстановления

Напишите тесты хоста в наборе исходных файлов test. Добавьте к каждому тестовому классу аннотацию @ExtendWith(BackupRestoreExtension::class), которая предоставляет подключенные устройства для тестов. Тесты выполняются для варианта приложения, на который нацелен набор, поэтому вам не нужно задавать идентификатор приложения.

Каждый метод тестирования объявляет параметр BackupRestoreController для каждого используемого устройства.

Тестирование встроенных типов хранилищ

Чтобы протестировать встроенные типы хранилищ, вызовите runBackupRestoreFlow с данными для заполнения, каталогом для файла резервной копии и транспортным режимом. Метод выполняет на устройстве следующие действия:

  1. Заполняет каждый домен хранилища.
  2. Останавливает приложение.
  3. Резервное копирование приложения.
  4. Удаляет данные приложения.
  5. Восстанавливает резервную копию.
  6. Проверяет, что каждый домен хранилища имеет начальное значение или отсутствует, если он заключен в StorageDomain.ExcludedData.

Если шаг не удается выполнить, метод выдает исключение, которое приводит к сбою теста. В следующем примере тестируются SharedPreferences, Preferences DataStore, SQLite, внутренний файл и исключенный файл с использованием локального транспорта резервного копирования:

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

При определении доменов хранения учитывайте следующее:

  • Preference принимает имя файла SharedPreferences без расширения .xml.
  • DataStorePreference принимает имя, переданное в preferencesDataStore. Используйте для value тот же тип Kotlin, что и для ключа настроек приложения, например 1.25f для floatPreferencesKey, чтобы приложение могло прочитать восстановленную запись.
  • TextFile и BinaryFile разрешают относительные пути в соответствии с filesDir вашего приложения.
  • Database записывает строку с помощью SQL и создает таблицу со столбцами TEXT, если она не существует. Если в приложении используется Room, протестируйте содержимое базы данных с помощью действий на устройстве, которые используют объекты доступа к данным, чтобы база данных соответствовала схеме Room.
  • Для ExcludedData требуется хотя бы один неисключенный домен хранилища в том же вызове runBackupRestoreFlow.
  • Если задать isDeviceProtectedStorage = true, то Preference, DataStorePreference, Database, TextFile и BinaryFile будут использовать хранилище, защищенное устройством, а не хранилище по умолчанию.

Запись действий на устройствах

Чтобы заполнить или проверить данные, которые не охватывают встроенные типы хранилищ, напишите действия устройства в наборе исходных файлов androidTest набора тестов. Реализуйте BackupDeviceAction и переопределите execute, чтобы запустить действие с Context вашего приложения и аргументами из теста хоста. Возвращает BackupDeviceActionResult, созданный с помощью BackupDeviceActionResult.success или BackupDeviceActionResult.failure, передавая сообщение об ошибке в failure.

Метод execute выполняется в фоновом потоке процесса приложения, поэтому он может блокировать выполнение до завершения операций чтения и записи. Чтобы получить доступ к инструментарию средства запуска тестов, например UiAutomation, вызовите InstrumentationRegistry.getInstrumentation.

В следующем примере показано, как задать и проверить настройку темы в Proto DataStore, где settingsDataStore – это свойство DataStore<Settings>, определенное в вашем приложении:

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

Чтобы запустить действия по заполнению и проверке в runBackupRestoreFlow, передайте их имена классов в 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,
        )
    }
}

Чтобы передать значения, сгенерированные во время заполнения, в действие проверки, передайте карту в BackupDeviceActionResult.success. CustomData объединяет эти записи с аргументами действия проверки.

Выполните каждый шаг процесса

Чтобы настроить процесс, например вызвать uninstallApp и installApk для тестирования обновления или вызвать launchApp, выполните каждый шаг самостоятельно:

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

Если действие с устройством не удается выполнить, runOnDevice возвращает BackupActionResult.Failure вместо того, чтобы вызывать исключение. Вызовите getOrThrow, чтобы получить возвращенную карту data в случае успеха или выдать IOException в случае неудачи.

Проверьте передачу данных между устройствами

Чтобы протестировать передачу между двумя устройствами, объявите параметр BackupRestoreController для каждого устройства и передайте целевое устройство в restoreOn в 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,
        )
    }
}

source – устройство, на котором приложение заполняется данными и создается резервная копия. target – устройство, на котором данные приложения удаляются, восстанавливаются из резервной копии и проверяются. Для восстановления на другом устройстве требуется DEVICE_TO_DEVICE или облачный транспортный режим, поскольку резервные копии данных LOCAL остаются на исходном устройстве.

Значения role параметра @Device позволяют выбрать устройство для каждого параметра при запуске тестов. Если подключено только одно подходящее устройство, оба параметра будут использовать его и симулировать перенос на одном устройстве.

Выберите устройства

По умолчанию тестовый фреймворк сортирует подключенные устройства по серийному номеру и сопоставляет каждому параметру BackupRestoreController отдельное устройство, если их подключено достаточно. Параметры, которые не могут получить отдельное устройство, используют первое подходящее устройство. Переменная среды ANDROID_SERIAL не влияет на назначение устройств.

Чтобы выбрать устройства, не указывая серийные номера в тестах, назначьте каждому параметру устройства роль с помощью @Device(role = "..."). Затем при запуске тестов свяжите каждую роль с устройством, используя системное свойство androidx.test.backup.device.serial.{role}. Например, следующая команда запускает набор тестов с emulator-5554 в качестве исходного устройства и emulator-5556 в качестве целевого устройства:

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

Привязка применяется только к параметрам с этой ролью. Другие параметры получают оставшиеся подходящие устройства.

Вы также можете фильтровать устройства по serial, api, minApi и maxApi на @Device, например @Device(role = "target", minApi = 35). Чтобы задать серийные номера или точные уровни API в командной строке, передайте следующие системные свойства с помощью -D:

Свойство системы Описание
androidx.test.backup.device.serial.{role} Серийный номер устройства для параметров, помеченных как @Device(role = "{role}"). Имеет приоритет над serials, serial и значением serial атрибута @Device.
androidx.test.backup.device.serials Список серийных номеров, разделенных запятыми, которые присвоены параметрам устройства по порядку. Имеет приоритет над serial и значением serial правила @Device.
androidx.test.backup.device.serial Серийный номер для параметров устройства, которые не охватывает serials. Имеет приоритет над значением serial правила @Device.
androidx.test.backup.device.apis Список уровней API, разделенный запятыми и назначенный параметрам устройства по порядку. Используйте 0, чтобы указать любой уровень API. Заменяет api на @Device в пределах minApi и maxApi.
androidx.test.backup.device.api Уровень API для параметров устройства, которые не охватывает apis. Используйте 0, чтобы указать любой уровень API. Заменяет api на @Device в пределах minApi и maxApi.

Свойства serial, serials, api и apis применяются ко всем запускаемым тестам, поэтому используйте параметр --tests, чтобы запускать только те тесты, которым они нужны.

Тестирование восстановления учетных данных

Если в вашем приложении используется восстановление учетных данных, проверьте, сохраняется ли ключ восстановления после резервного копирования и восстановления. В режимах облачного резервного копирования и переноса данных с устройства на устройство ключ сохраняется вместе с данными приложения, а в режиме LOCAL – нет.

Напишите действия устройства, которые создают ключ и получают его с помощью Менеджера учетных данных. Действие create возвращает идентификатор учетных данных ключа, чтобы действие verify могло проверить, что восстановление возвращает тот же ключ. В этом примере createRequestJson и getRequestJson – это строки JSON запроса, которые ваше приложение передает Менеджеру учетных данных.

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

Следующий тест хоста создает ключ на исходном устройстве, восстанавливает его на целевом устройстве и проверяет, совпадает ли идентификатор учетных данных:

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

Чтобы создать ключ, у которого для параметра isCloudBackupEnabled задано значение по умолчанию true, на исходном устройстве должен быть выполнен вход в аккаунт Google с включенным резервным копированием и настроена блокировка экрана. В противном случае Менеджер учетных данных выдаст ошибку E2eeUnavailableException.

В отличие от восстановления из облака на новом устройстве, для облачных режимов не требуется тот же аккаунт Google или способ разблокировки экрана исходного устройства. Кроме того, они восстанавливают ключи, созданные с помощью isCloudBackupEnabled, для которого задано значение false.

Сохранять данные приложения между тестами

Перед каждым тестированием фреймворк удаляет данные приложения на всех устройствах, которые используются для проверки. Чтобы отключить эту функцию для тестового класса или метода, добавьте к нему аннотацию @Isolation(IsolationPolicy.MANUAL).

Как писать тесты на Java

Методы BackupRestoreController – это функции приостановки Kotlin. Если вы пишете хост-тесты на Java, используйте вариант Async каждого метода, например runBackupRestoreFlowAsync, который возвращает ListenableFuture.

Как запустить тесты резервного копирования и восстановления

AGP создает задачу Gradle для каждого варианта, который тестирует набор, с именем test{Variant}{SuiteName}DefaultTestSuite. Чтобы запустить набор тестов backupTest для варианта debug, подключите устройства и выполните задачу:

./gradlew :app:testDebugBackupTestDefaultTestSuite

Задачи check, test и connectedCheck не запускают наборы тестов для резервного копирования и восстановления, поэтому запустите задачу набора тестов вручную.

Чтобы запустить один тестовый класс или метод, используйте параметр --tests:

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

Как изучить отчеты о тестировании

После завершения тестового запуска AGP создает отчет о тестировании в формате HTML по пути {module}/build/reports/tests/{taskName}/index.html, например app/build/reports/tests/testDebugBackupTestDefaultTestSuite/index.html.

Если тест завершается неудачно, фреймворк сохраняет logcat с каждого устройства, которое использовалось в тесте, и перечисляет пути к сохраненным файлам в выходных данных о сбое. Чтобы отключить сохранение журналов, передайте -Dandroidx.test.backup.device.logs.enabled=false.

Устранение неполадок при восстановлении

Восстановление из облака или с другого устройства завершается с сообщением об ошибке в следующих случаях:

  • Сервисы Google Play пропускают ваше приложение, например потому что его идентификатор начинается с com.google. или com.android., резервная копия пуста или в нем задан атрибут android:allowBackup="false". Чтобы протестировать приложение с таким идентификатором, используйте режим LOCAL.
  • Версия сервисов Google Play ниже 26.33.30 на устройстве или эмуляторе с API уровня 36.1 или выше. Обновите сервисы Google Play через Google Play на устройстве или используйте образ системы с версией 26.33.30 или более поздней.

Известные проблемы и ограничения

При тестировании автоматического резервного копирования и восстановления могут возникать следующие проблемы и ограничения:

  • Поддержка наборов тестов в Android Studio пока недоступна. Редактор кода может показывать ошибки неразрешенных ссылок в исходных файлах набора тестов, даже если проект настроен правильно. Вместо этого создавайте и запускайте тесты резервного копирования и восстановления из командной строки с помощью Gradle.
  • Тесты выполняются только на устройствах и эмуляторах, подключенных к компьютеру для разработки через adb. Firebase Test Lab не поддерживается.
  • В ходе тестирования данные приложения удаляются, а настройки резервного копирования на устройствах изменяются. Рекомендуем использовать для тестирования отдельные устройства.

Информацию об изменениях в каждом выпуске можно найти в примечаниях к выпуску тестирования резервного копирования и восстановления.