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

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

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

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

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

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

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

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

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

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

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

Виды транспорта

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

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

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

Требования

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

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

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

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

Компонент Минимальная версия Рекомендуемая версия
Android Studio Rabbit 2, Canary 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-alpha02 или более поздней версии

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

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

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

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

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

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

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

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

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

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

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

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

Чтобы добавить другие зависимости, используйте блоки 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) предоставляет подключенные устройства для тестирования.
  • @BackupRestoreConfig(applicationId = "...") задает идентификатор тестируемого приложения. Используйте идентификатор приложения для тестируемого варианта, включая applicationIdSuffix.

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

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

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

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

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

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

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

  • Preference принимает название файла SharedPreferences без расширения .xml.
  • TextFile и BinaryFile разрешают относительные пути к filesDir приложения.
  • Database записывает строку с помощью SQL и создает таблицу с TEXT столбцами, если таблица не существует. Если в вашем приложении используется Room, мы рекомендуем тестировать содержимое базы данных с помощью действий на устройстве, которые используют ваши объекты доступа к данным, чтобы у базы данных была схема, ожидаемая Room.

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

Чтобы заполнить или проверить данные, которые не охватываются встроенными типами хранилищ, напишите действия устройства в наборе исходных файлов androidTest набора тестов. Действие устройства реализует BackupDeviceAction и переопределяет два элемента:

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

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

В следующем примере показано, как задать и проверить настройку темы, которую приложение сохраняет с помощью Preferences DataStore. Приложение определяет DataStore в основном наборе исходных файлов:

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

Для действий с устройствами используются те же определения:

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

Чтобы вернуть данные в хост-тест, передайте карту методу success. Тест хоста считывает карту из BackupActionResult.Success.data.

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

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

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

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

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

Чтобы проверить передачу между двумя устройствами, объявите параметр BackupRestoreController для каждого из них. Создайте резервную копию приложения на исходном устройстве, а затем восстановите файл резервной копии на целевом устройстве. В следующем примере используются константы и вспомогательный метод assertSuccess из предыдущего примера.

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

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

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

По умолчанию фреймворк сортирует устройства по серийному номеру и назначает их параметрам BackupRestoreController по позиции. Например, первый параметр тестового метода получает первое устройство, а второй параметр – второе. Если для позиции нет устройства, параметр получает первое устройство. Позиции учитывают все параметры метода, поэтому объявляйте параметры устройства перед другими параметрами, такими как @TempDir. Переменная среды ANDROID_SERIAL не влияет на назначение устройств.

Чтобы выбрать устройства, задайте значения serial или api для @Device или задайте системные свойства JVM. Системные свойства имеют приоритет над значениями @Device. В таблице ниже перечислены поддерживаемые системные свойства.

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

Передайте эти свойства с помощью -D при запуске тестов из командной строки. Свойства применяются ко всем выполняемым тестам, поэтому используйте вариант --tests, чтобы запускать только те тесты, которым они нужны. Например, следующая команда запускает ThemeTransferTest с emulator-5554 в качестве исходного устройства и emulator-5556 в качестве целевого устройства:

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

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

Перед каждым тестированием фреймворк удаляет данные приложения на всех устройствах, которые используются для тестирования. Чтобы отключить эту функцию для тестового класса или метода, добавьте к нему аннотацию @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.

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

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

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

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