Automatyczne testowanie tworzenia i przywracania kopii zapasowych

Gdy użytkownicy skonfigurują nowe urządzenie lub ponownie zainstalują Twoją aplikację, Android może przywrócić dane aplikacji z kopii zapasowej w chmurze lub przenieść je z urządzenia na urządzenie. Jeśli reguły tworzenia kopii zapasowej pomijają ważne dane lub aplikacja nie może używać przywróconych danych, użytkownicy tracą ustawienia i postępy bez wyświetlania błędu.

Automatyczne testy tworzenia i przywracania kopii zapasowych pomagają wykryć te problemy przed opublikowaniem aplikacji. Są to testy JUnit 5, które działają na komputerze używanym do programowania i sterują połączonymi urządzeniami za pomocą Android Debug Bridge (adb). Każdy test wypełnia urządzenie danymi aplikacji, tworzy kopię zapasową i przywraca aplikację za pomocą platformy tworzenia kopii zapasowych Androida, a następnie sprawdza, czy dane są nienaruszone. Testy konfigurujesz jako pakiet testów wtyczki Androida do obsługi Gradle (AGP) w pliku build modułu.

Jak to działa

Pakiet testów tworzenia i przywracania kopii zapasowych składa się z 2 części, które współpracują ze sobą:

  • Testy hosta to testy JUnit 5, które działają na komputerze używanym do programowania. Używają one BackupRestoreController na każdym urządzeniu do wypełniania danych, tworzenia kopii zapasowej i przywracania aplikacji oraz sprawdzania wyników.
  • Działania na urządzeniu to klasy, które implementują BackupDeviceAction. Działają one w procesie aplikacji na urządzeniu, dzięki czemu mogą odczytywać i zapisywać dane za pomocą kodu aplikacji. Testy hosta wywołują działania na urządzeniu według nazwy klasy.

Gdy uruchamiasz pakiet, AGP tworzy aplikację i testowy plik APK, który zawiera działania na urządzeniu. Przed każdym testem platforma testowa instaluje oba pliki APK na każdym urządzeniu, którego używa test, a następnie czyści dane aplikacji.

Wbudowane typy pamięci masowej

W tabeli poniżej znajdziesz StorageDomain podklasy, które umożliwiają wypełnianie i weryfikowanie danych bez pisania działań na urządzeniu:

Domena pamięci masowej Dane
StorageDomain.Preference Klucz w pliku SharedPreferences.
StorageDomain.Database Wiersz w tabeli bazy danych SQLite.
StorageDomain.TextFile Plik tekstowy w pamięci wewnętrznej aplikacji.
StorageDomain.BinaryFile Plik binarny w pamięci wewnętrznej aplikacji.

Aby testować inne dane, np. pliki DataStore, napisz działania na urządzeniu.

Środki transportu

Każda kopia zapasowa używa wartości BackupTransportMode. W tabeli poniżej znajdziesz obsługiwane środki transportu:

Tryb Scenariusz
LOCAL Kopia zapasowa w lokalnym środku transportu kopii zapasowej urządzenia. Dane kopii zapasowej pozostają na urządzeniu.
CLOUD_ENCRYPTED Kopia zapasowa w chmurze, np. zapisana na koncie Google użytkownika z urządzenia z szyfrowaniem po stronie klienta.
CLOUD_UNENCRYPTED Kopia zapasowa w chmurze z urządzenia, które nie ma szyfrowania po stronie klienta.
DEVICE_TO_DEVICE Przesyłanie z urządzenia na urządzenie, np. przez kabel lub Wi-Fi.

LOCAL działa na każdym obsługiwanym urządzeniu. Tryby chmury i przesyłania z urządzenia na urządzenie tworzą te same kopie zapasowe co działanie Kopia zapasowa danych aplikacji w Android Studio: kopia zapasowa jest zapisywana na komputerze używanym do programowania zamiast przesyłania lub wysyłania na inne urządzenie. Tryby chmury i przesyłania z urządzenia na urządzenie wymagają Usług Google Play na urządzeniu i kompilacji aplikacji z możliwością debugowania. Jeśli reguły tworzenia kopii zapasowej zależą od szyfrowania po stronie klienta, przetestuj oba tryby chmury.

Wymagania

Aby korzystać z automatycznego testowania tworzenia i przywracania kopii zapasowych, upewnij się, że Twoje środowisko spełnia te wymagania:

  • Gradle w wersji 9.6.0 lub nowszej.
  • JDK w wersji 17 lub nowszej.
  • Urządzenie lub emulator z Androidem 12 (poziom interfejsu API 31) lub nowszym, które jest połączone przez adb. Aby przetestować przesyłanie między 2 urządzeniami, połącz 2 urządzenia.

Zgodność

W tabeli poniżej znajdziesz minimalne i zalecane wersje automatycznego testowania tworzenia i przywracania kopii zapasowych:

Komponent Wersja minimalna Wersja zalecana
Android Studio Rabbit 2 Canary 1 Rabbit 2 Canary 4 lub nowsza
Wtyczka Androida do obsługi Gradle (AGP) 9.5.0-alpha06 9.5.0-alpha09 lub nowsza
Biblioteki androidx.test.backup 1.0.0-alpha01 1.0.0-alpha02 lub nowsza

Każda wersja testowa AGP wymaga odpowiedniej wersji testowej Android Studio. Więcej informacji znajdziesz w artykule Wersje testowe AGP.

Konfigurowanie projektu

Aby dodać zestaw testów tworzenia i przywracania kopii zapasowych do modułu aplikacji, wykonaj te czynności:

Włączanie flag eksperymentalnych

W pliku gradle.properties projektu włącz zestawy testów i testy kopii zapasowych:

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

Deklarowanie zestawu testów

W pliku build.gradle.kts modułu aplikacji zadeklaruj zestaw testów kopii zapasowych w bloku testOptions:

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

Nazwa pakietu, w tym przykładzie backupTest, określa katalogi zbioru źródeł i nazwy zadań Gradle. Pakiet możesz dostosować za pomocą tych opcjonalnych właściwości:

  • Użyj backupTestLibraryVersion, aby ustawić wersję bibliotek androidx.test.backup, które AGP dodaje do pakietu, np. "1.0.0-alpha02". Ta właściwość jest opcjonalna. Jeśli ją pominiesz, AGP użyje domyślnej wersji biblioteki na podstawie wersji AGP.
  • Użyj targetVariants, aby określić warianty kompilacji, które testuje pakiet, takie jak targetVariants.add("demoDebug"). Ta właściwość jest opcjonalna. Jeśli ją pominiesz, pakiet będzie testować warianty testBuildType modułu, który domyślnie jest ustawiony na debug. Zalecamy testowanie wariantu z możliwością debugowania, ponieważ tryby transportowe w chmurze i przesyłania z urządzenia na urządzenie wymagają aplikacji z możliwością debugowania.

AGP dodaje do testów hosta bibliotekę hosta androidx.test.backup:backup-host i JUnit 5. Dodaje bibliotekę urządzenia androidx.test.backup:backup do testowego pliku APK i ustawia narzędzie do uruchamiania instrumentacji testowego pliku APK.

Aby dodać inne zależności, użyj bloków hostJar i testApk pakietu. Aby na przykład używać Truth w testach hosta, dodaj go do hostJar bloku:

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

Tworzenie zbiorów źródeł

Utwórz katalogi zbioru źródeł, które pasują do nazwy pakietu:

  • Umieść testy hosta w src/{suiteName}/test/kotlin/.
  • Umieść działania na urządzeniu w src/{suiteName}/androidTest/kotlin/.

Na przykład pakiet o nazwie backupTest używa tej struktury katalogów:

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

Sprawdzanie konfiguracji kopii zapasowej

Testy tworzą kopię zapasową i przywracają dane, które są uwzględnione w konfiguracji kopii zapasowej aplikacji. Upewnij się, że tworzenie kopii zapasowej jest włączone w aplikacji i że twoje reguły tworzenia kopii zapasowej obejmują dane, które testujesz.

Pisanie testów tworzenia i przywracania kopii zapasowych

Napisz testy hosta w zbiorze źródeł test pakietu. Każda klasa testowa wymaga 2 adnotacji:

  • @ExtendWith(BackupRestoreExtension::class) udostępnia testom podłączone urządzenia.
  • @BackupRestoreConfig(applicationId = "...") ustawia identyfikator aplikacji testowanej aplikacji. Użyj identyfikatora aplikacji wariantu, który testujesz, w tym dowolnego applicationIdSuffix.

Każda metoda testowa deklaruje parametr BackupRestoreController dla każdego urządzenia, którego używa.

Testowanie wbudowanych typów pamięci masowej

Aby testować wbudowane typy pamięci masowej, wywołaj runBackupRestoreFlow z danymi do wypełnienia, katalogiem pliku kopii zapasowej i środkiem transportu. Metoda wykonuje na urządzeniu te czynności:

  1. Wypełnia każdą domenę pamięci masowej.
  2. Zatrzymuje aplikację.
  3. Tworzy kopię zapasową aplikacji.
  4. Czyści dane aplikacji.
  5. Przywraca kopię zapasową.
  6. Sprawdza, czy każda domena pamięci masowej ma wartość ziarna.

Jeśli któryś z tych kroków się nie powiedzie, metoda zgłosi wyjątek, co spowoduje niepowodzenie testu. Ten przykład testuje klucz SharedPreferences, wiersz SQLite i plik wewnętrzny za pomocą lokalnego środka transportu kopii zapasowej:

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

Podczas definiowania domen pamięci masowej pamiętaj o tych kwestiach:

  • Preference przyjmuje nazwę pliku SharedPreferences bez rozszerzenia .xml.
  • TextFile i BinaryFile rozwiązują ścieżki względne względem filesDir aplikacji.
  • Database zapisuje wiersz za pomocą SQL i tworzy tabelę z kolumnami TEXT, jeśli tabela nie istnieje. Jeśli aplikacja używa Room, zalecamy testowanie zawartości bazy danych za pomocą działań na urządzeniu które używają obiektów DAO, aby baza danych miała schemat oczekiwany przez Room.

Pisanie działań na urządzeniu

Aby wypełnić lub zweryfikować dane, których nie obejmują wbudowane typy pamięci masowej, napisz działania na urządzeniu w zbiorze źródeł androidTest pakietu. Działanie na urządzeniu implementuje BackupDeviceAction i zastępuje 2 elementy:

  • Ustaw phase na BackupDeviceAction.PHASE_POPULATE w przypadku działania, które wypełnia dane, lub BackupDeviceAction.PHASE_VERIFY w przypadku działania, które sprawdza dane.
  • Zaimplementuj execute, aby uruchomić działanie z Context aplikacji i argumentami z testu hosta. Zwróć wynik utworzony za pomocą BackupDeviceActionResult.success lub BackupDeviceActionResult.failure metody fabrycznej i przekaż komunikat o błędzie do failure.

Metoda execute działa w procesie aplikacji w wątku w tle, więc może się blokować do czasu zakończenia odczytu i zapisu.

Ten przykład wypełnia i weryfikuje ustawienie motywu, które aplikacja przechowuje za pomocą Preferences DataStore. Aplikacja definiuje DataStore w głównym zbiorze źródeł:

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

Działania na urządzeniu używają tych samych definicji:

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

Aby zwrócić dane do testu hosta, przekaż mapę do metody success. Test hosta odczytuje mapę z BackupActionResult.Success.data.

Uruchamianie każdego kroku procesu

Aby używać działań na urządzeniu lub dostosować proces, wywołaj każdy krok samodzielnie. Ten test hosta uruchamia działania na urządzeniu z poprzedniej sekcji w ramach tworzenia i przywracania kopii zapasowej w chmurze:

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

Gdy działanie na urządzeniu się nie powiedzie, runOnDevice zwraca a BackupActionResult.Failure zamiast zgłaszać wyjątek, więc sprawdź wynik. Pozostałe kroki zgłaszają wyjątek, jeśli się nie powiodą.

Testowanie przesyłania między urządzeniami

Aby przetestować przesyłanie między 2 urządzeniami, zadeklaruj parametr BackupRestoreController dla każdego urządzenia. Utwórz kopię zapasową aplikacji na urządzeniu źródłowym, a następnie przywróć plik kopii zapasowej na urządzeniu docelowym. Ten przykład ponownie używa stałych i pomocnika assertSuccess z poprzedniego przykładu:

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

Wartość role w @Device to etykieta, która ułatwia odczytanie testu. Nie wpływa ona na to, które urządzenie otrzyma parametr. Jeśli podłączone jest tylko 1 urządzenie, oba parametry używają tego urządzenia, a test symuluje przesyłanie na jednym urządzeniu. Wyczyszczenie danych urządzenia docelowego gwarantuje, że przywrócone dane pochodzą z kopii zapasowej.

Wybieranie urządzeń

Domyślnie platforma testowa sortuje urządzenia online według numeru seryjnego i przypisuje je do parametrów BackupRestoreController według pozycji. Na przykład pierwszy parametr metody testowej otrzymuje pierwsze urządzenie, a drugi parametr – drugie urządzenie. Jeśli na danej pozycji nie ma urządzenia, parametr otrzymuje pierwsze urządzenie. Pozycje zliczają wszystkie parametry metody, więc deklaruj parametry urządzenia przed innymi parametrami, np. @TempDir. Zmienna środowiskowa ANDROID_SERIAL nie wpływa na przypisanie urządzenia.

Aby wybrać urządzenia, ustaw wartości serial lub api w @Device albo ustaw właściwości systemowe JVM. Właściwości systemowe mają pierwszeństwo przed wartościami @Device. W tabeli poniżej znajdziesz obsługiwane właściwości systemowe:

Właściwość systemowa Opis
androidx.test.backup.device.serial Numer seryjny urządzenia dla wszystkich parametrów urządzenia.
androidx.test.backup.device.serials Lista rozdzielona przecinkami numerów seryjnych, przypisanych do parametrów według pozycji.
androidx.test.backup.device.serial.{name} Numer seryjny parametrów oznaczonych adnotacją @Device(serial = "{name}").
androidx.test.backup.device.api Poziom interfejsu API urządzenia dla wszystkich parametrów urządzenia.
androidx.test.backup.device.apis Lista poziomów interfejsu API rozdzielonych przecinkami, przypisanych do parametrów według pozycji.

Przekaż te właściwości za pomocą -D, gdy uruchamiasz testy z wiersza poleceń. Właściwości dotyczą każdego uruchamianego testu, więc użyj opcji --tests, aby uruchamiać tylko testy, które ich potrzebują. Na przykład to polecenie uruchamia ThemeTransferTest z emulator-5554 jako urządzeniem źródłowym i emulator-5556 jako urządzeniem docelowym:

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

Zachowywanie danych aplikacji między testami

Przed każdym testem platforma testowa czyści dane aplikacji na każdym urządzeniu, którego używa test. Aby wyłączyć tę opcję w przypadku klasy lub metody testowej, oznacz ją adnotacją @Isolation(IsolationPolicy.MANUAL).

Pisanie testów w Javie

Metody BackupRestoreController to funkcje zawieszające Kotlin. Jeśli piszesz testy hosta w Javie, użyj wariantu Async każdej metody, np. runBackupRestoreFlowAsync, która zwraca ListenableFuture.

Uruchamianie testów tworzenia i przywracania kopii zapasowych

AGP tworzy zadanie Gradle dla każdego wariantu, który testuje pakiet, o nazwie test{Variant}{SuiteName}DefaultTestSuite. Aby uruchomić pakiet backupTest w wariancie debug, połącz urządzenia i uruchom zadanie:

./gradlew :app:testDebugBackupTestDefaultTestSuite

Zadania check, test i connectedCheck nie uruchamiają pakietów testów tworzenia i przywracania kopii zapasowych, więc uruchom zadanie pakietu w sposób jawny.

Aby uruchomić pojedynczą klasę lub metodę testową, użyj opcji --tests:

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

Sprawdzanie raportów z testów

Po zakończeniu testu AGP generuje raport z testu w formacie HTML w lokalizacji {module}/build/reports/tests/{taskName}/index.html, np. app/build/reports/tests/testDebugBackupTestDefaultTestSuite/index.html.

Znane problemy i ograniczenia

Automatyczne testowanie tworzenia i przywracania kopii zapasowych ma te znane problemy i ograniczenia:

  • Obsługa pakietów testów w Android Studio nie jest jeszcze dostępna. Edytor kodu może wyświetlać błędy nierozwiązanych odwołań w plikach źródłowych pakietu testów, nawet jeśli projekt jest prawidłowo skonfigurowany. Zamiast tego kompiluj i uruchamiaj testy tworzenia i przywracania kopii zapasowych z wiersza poleceń za pomocą Gradle.
  • Testy działają tylko na urządzeniach i emulatorach, które są połączone z komputerem używanym do programowania przez narzędzie adb. Laboratorium Firebase nie jest obsługiwane.
  • Testy czyszczą dane aplikacji i zmieniają ustawienia kopii zapasowej na urządzeniach, których używają. Zalecamy używanie dedykowanych urządzeń testowych.

Informacje o zmianach w każdej wersji znajdziesz w informacjach o wersji testowania tworzenia i przywracania kopii zapasowych.