バックアップと復元の自動テスト

ユーザーが新しいデバイスをセットアップしたり、アプリを再インストールしたりすると、Android はクラウド バックアップまたはデバイス間転送からアプリのデータを復元できます。 バックアップ ルールで重要なデータが除外されている場合や、アプリが復元したデータ を使用できない場合、ユーザーはエラーが表示される ことなく設定と進行状況を失います。

自動バックアップと復元のテストは、アプリをリリースする前にこれらの問題を検出するのに役立ちます。これは、開発マシンで実行され、Android Debug Bridge(adb)を介して接続されたデバイスを制御する JUnit 5 テストです。各テストでは、デバイスにアプリデータをシードし、Android バックアップ フレームワークを介してアプリをバックアップして復元し、データがそのまま残っていることを確認します。テストは、モジュールのビルドファイルで Android Gradle プラグイン(AGP)テストスイートとして構成します。

仕組み

バックアップと復元のテストスイートは、連携して動作する次の 2 つの部分で構成されています。

  • ホストテスト は、開発マシンで実行される JUnit 5 テストです。各デバイスの BackupRestoreController を使用して、データのシード、アプリのバックアップと復元、結果の確認を行います。
  • デバイス アクション は、 BackupDeviceAction を実装するクラスです。デバイス上のアプリのプロセスで実行されるため、アプリ独自のコードを介してデータの読み取りと書き込みを行うことができます。ホストテストは、クラス名でデバイス アクションを呼び出します。

スイートを実行すると、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 は、サポートされているすべてのデバイスで動作します。クラウド モードとデバイス間モードでは、Android Studio の [アプリデータをバックアップ] アクションと同じバックアップが作成されます。バックアップは、アップロードされたり別のデバイスに送信されたりするのではなく、開発マシンに保存されます。クラウド モードとデバイス間モードでは、デバイスに Google Play 開発者サービスとアプリのデバッグ可能なビルドが必要です。バックアップ ルールがクライアントサイド暗号化に依存している場合は、両方のクラウド モードをテストします。

要件

自動バックアップと復元のテストを使用するには、環境が次の要件を満たしていることを確認してください。

  • Gradle バージョン 9.6.0 以降。
  • JDK バージョン 17 以降。
  • adb 経由で接続されている Android 12(API レベル 31)以降を搭載したデバイスまたはエミュレータ。2 台のデバイス間の転送をテストするには、2 台のデバイスを接続します。

互換性

次の表に、自動バックアップと復元のテストの最小バージョンと推奨バージョンを示します。

コンポーネント 最小バージョン 推奨バージョン
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 を使用して、AGP がスイートに追加する androidx.test.backup ライブラリのバージョン( "1.0.0-alpha02" など)を設定します。このプロパティは省略可能です。省略すると、AGP は AGP バージョンに基づくデフォルトのライブラリ バージョンにフォールバックします。
  • targetVariants を使用して、スイートがテストするビルド バリアント( たとえば targetVariants.add("demoDebug"))を指定します。このプロパティは省略可能です。省略すると、スイートはモジュールの testBuildType のバリアントをテストします。デフォルトは debug です。クラウド モードとデバイス間転送モードではデバッグ可能なアプリが必要なため、デバッグ可能なバリアントをテストすることをおすすめします。

AGP は、ホスト ライブラリ androidx.test.backup:backup-host と JUnit 5 をホストテストに追加します。デバイス ライブラリ androidx.test.backup:backup をテスト APK に追加し、テスト 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 ソースセットにホストテストを記述します。各テストクラスには 2 つのアノテーションが必要です。

  • @ExtendWith(BackupRestoreExtension::class) は、接続されたデバイスをテストに提供します。
  • @BackupRestoreConfig(applicationId = "...") は、テスト対象のアプリのアプリケーション ID を設定します。applicationIdSuffix を含む、テストするバリアントのアプリケーション ID を使用します。

各テストメソッドは、使用するデバイスごとに 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 を実装し、次の 2 つのメンバーをオーバーライドします。

  • データをシードするアクションの場合は 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 を例外をスローする代わりに返すため、 結果を確認します。他のステップが失敗すると、例外がスローされます。

デバイス間の転送をテストする

2 台のデバイス間の転送をテストするには、デバイスごとに 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()
    }
}

role 値は、@Device のテストを読みやすくするラベルです。パラメータが取得するデバイスには影響しません。接続されているデバイスが 1 台のみの場合、両方のパラメータはそのデバイスを使用し、テストは 1 台のデバイスでの転送をシミュレートします。ターゲットのデータを消去すると、復元されたデータがバックアップから取得されるようになります。

デバイスの選択

デフォルトでは、テスト フレームワークはオンライン デバイスをシリアル番号で並べ替え、位置ごとに BackupRestoreController パラメータに割り当てます。たとえば、テストメソッドの最初のパラメータは最初のデバイスを取得し、2 番目のパラメータは 2 番目のデバイスを取得します。位置にデバイスがない場合、パラメータは最初のデバイスを取得します。位置はメソッドのすべてのパラメータをカウントするため、@TempDir などの他のパラメータの前にデバイス パラメータを宣言します。環境変数 ANDROID_SERIAL は、デバイスの割り当てには影響しません。

デバイスを選択するには、@Device の serial 値または api 値を設定するか、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 オプションを使用して、必要なテストのみを実行します。たとえば、次のコマンドは、ソースデバイスとして emulator-5554、対象デバイスとして emulator-5556 を使用して ThemeTransferTest を実行します。

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

テスト間でアプリデータを保持する

各テストの前に、テスト フレームワークはテストで使用する各デバイスでアプリのデータを消去します。テストクラスまたはメソッドでこれを無効にするには、@Isolation(IsolationPolicy.MANUAL) でアノテーションを付けます。

Java でテストを作成する

BackupRestoreController メソッドは Kotlin の suspend 関数です。Java でホストテストを作成する場合は、各メソッドの Async バリアント(runBackupRestoreFlowAsync など)を使用します。これは ListenableFuture を返します。

バックアップと復元のテストを実行する

AGP は、スイートがテストするバリアントごとに test{Variant}{SuiteName}DefaultTestSuite という名前の Gradle タスクを作成します。debug バリアントに対して backupTest スイートを実行するには、デバイスを接続してタスクを実行します。

./gradlew :app:testDebugBackupTestDefaultTestSuite

check、test、connectedCheck タスクはバックアップと復元のテストスイートを実行しないため、スイートのタスクを明示的に実行します。

単一のテストクラスまたはメソッドを実行するには、 --tests オプションを使用します。

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

テストレポートを確認する

テストの実行が完了すると、AGP は {module}/build/reports/tests/{taskName}/index.html(app/build/reports/tests/testDebugBackupTestDefaultTestSuite/index.html など)に HTML テストレポートを生成します。

既知の問題と制限事項

自動バックアップと復元のテストには、次の既知の問題と制限事項があります。

  • Android Studio でのテストスイートのサポートはまだ提供されていません。プロジェクトが正しく構成されている場合でも、コードエディタにテストスイートのソースファイルで未解決の参照エラーが表示されることがあります。代わりに、Gradle を使用してコマンドラインからバックアップと復元のテストをビルドして実行します。
  • テストは、adb 経由で開発マシンに接続されているデバイスとエミュレータでのみ実行されます。Firebase Test Lab はサポートされていません。
  • テストでは、使用するデバイスでアプリのデータが消去され、バックアップ設定が変更されます。専用のテストデバイスを使用することをおすすめします。

各リリースの変更については、 バックアップと復元のテストのリリースノートをご覧ください。