آزمایش خودکار پشتیبان‌گیری و بازیابی

وقتی کاربران دستگاه جدیدی را راه‌اندازی می‌کنند یا برنامه‌تان را بازنصب می‌کنند، Android می‌تواند داده‌های برنامه‌تان را از نسخه پشتیبان ابری یا انتقال دستگاه‌به‌دستگاه بازیابی کند. اگر قوانین پشتیبان‌گیری شما داده‌های مهم را حذف کند، یا برنامه‌تان نتواند از داده‌هایی که بازیابی می‌کند استفاده کند، کاربران تنظیمات و پیشرفت خود را بدون دیدن خطا ازدست می‌دهند.

آزمایش‌های خودکارسازی‌شده پشتیبان‌گیری و بازیابی به شما کمک می‌کنند این مشکلات را قبل‌از انتشار برنامه‌تان شناسایی کنید. این آزمایش‌ها JUnit 5 هستند که در ماشین توسعه نرم‌افزار شما اجرا می‌شوند و دستگاه‌های متصل‌شده را ازطریق «پل اشکال‌زدایی Android»‏ (adb) کنترل می‌کنند. هر آزمایش داده‌های برنامه را در دستگاهی قرار می‌دهد، از برنامه ازطریق چارچوب پشتیبان‌گیری Android پشتیبان‌گیری و بازیابی می‌کند، و سپس بررسی می‌کند که داده‌ها دست‌نخورده باشند. آزمایش‌ها را به‌عنوان مجموعه آزمون افزایه Android Gradle (AGP) در فایل ساخت واحدتان پیکربندی می‌کنید.

روش کار

مجموعه آزمون پشتیبان‌گیری و بازیابی دو بخش دارد که باهم کار می‌کنند:

  • آزمایش‌های میزبان آزمایش‌های JUnit 5 هستند که در ماشین توسعه نرم‌افزار شما اجرا می‌شوند. آن‌ها از BackupRestoreController برای هر دستگاه برای کاشت داده، پشتیبان‌گیری و بازیابی برنامه شما، و بررسی نتایج استفاده می‌کنند.
  • کنش‌های دستگاه کلاس‌هایی هستند که BackupDeviceAction را پیاده‌سازی می‌کنند. این کیت‌های توسعه نرم‌افزار در فرایند برنامه‌تان در دستگاه اجرا می‌شوند، بنابراین می‌توانند داده‌ها را ازطریق کد خود برنامه‌تان بخوانند و بنویسند. Host tests call device actions by class name.

وقتی مجموعه را اجرا می‌کنید، 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 نسخه ۹.۶.۰ یا بالاتر.
  • نسخه ۱۷ یا بالاتر JDK.
  • دستگاه یا شبیه‌سازی که Android نسخه ۱۲ (سطح میانای برنامه‌سازی کاربردی ۳۱) یا بالاتر را اجرا می‌کند و ازطریق adb متصل شده است:
    • دستگاه‌ها یا شبیه‌سازهای دارای سطح میانای برنامه‌سازی کاربردی ۳۶.۱ یا بالاتر به خدمات Google Play نسخه ۲۶.۳۳.۳۰ یا بالاتر نیاز دارند.
    • برای آزمایش انتقال بین دو دستگاه، دو دستگاه را متصل کنید.

سازگاری

جدول زیر حداقل نسخه‌ها و نسخه‌های توصیه‌شده برای آزمایش خودکار پشتیبان‌گیری و بازیابی را فهرست می‌کند:

مؤلفه حداقل نسخه نسخه توصیه‌شده
استودیو Android 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-alpha04 یا بالاتر

هر نسخه پیش‌نمایش AGP به نسخه پیش‌نمایش منطبق Android Studio نیاز دارد. برای اطلاعات بیشتر، به پیش‌نمایش‌های AGP مراجعه کنید.

راه‌اندازی پروژه

برای افزودن مجموعه آزمون پشتیبان‌گیری و بازیابی به واحد برنامه‌تان، مراحل زیر را تکمیل کنید:

فعال کردن پرچم‌های آزمایشی

در فایل 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 آزمایشی اضافه می‌کند و اجراکننده ابزار دقیق فایل 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 منتقل شده است می‌گیرد. از همان نوع Kotlin برای value به‌عنوان کلید اولویت برنامه‌تان استفاده کنید، مثلاً 1.25f برای floatPreferencesKey، تا برنامه‌تان بتواند ورودی بازیابی‌شده را بخواند.
  • TextFile و BinaryFile مسیرهای نسبی را درمقابل filesDir برنامه‌تان حل می‌کنند.
  • Database ردیف را با SQL می‌نویسد و اگر جدول وجود نداشته باشد، جدول را با ستون‌های TEXT ایجاد می‌کند. اگر برنامه‌تان از Room استفاده می‌کند، محتوای پایگاه داده را با کنش‌های دستگاه که از DAO شما استفاده می‌کنند آزمایش کنید تا پایگاه داده با طرح‌واره Room مطابقت داشته باشد.
  • ExcludedData به حداقل یک دامنه فضای ذخیره‌سازی مستثنی‌نشده در همان فراخوانی runBackupRestoreFlow نیاز دارد.
  • ‫Preference،‏ DataStorePreference،‏ Database،‏ TextFile، و BinaryFile وقتی isDeviceProtectedStorage = true را تنظیم می‌کنید، به‌جای فضای ذخیره‌سازی پیش‌فرض برنامه‌تان، فضای ذخیره‌سازی محافظت‌شده دستگاه را هدف قرار می‌دهند.

نوشتن کنش‌های دستگاه

برای کاشتن یا درستی‌سنجی داده‌هایی که انواع فضای ذخیره‌سازی داخلی پوشش نمی‌دهند، کنش‌های دستگاه را در مجموعه منبع 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 را برمی‌گرداند. برای دریافت نقشه data برگردانده‌شده درصورت موفقیت، getOrThrow را فراخوانی کنید یا درصورت ناموفق بودن، 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). برای تنظیم شماره سریال یا سطح دقیق میانای برنامه‌سازی کاربردی از خط فرمان، خصوصیات سیستم زیر را با -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 را در @Device، در minApi و maxApi جایگزین می‌کند.
androidx.test.backup.device.api سطح میانای برنامه‌سازی کاربردی برای پارامترهای دستگاهی که apis پوشش نمی‌دهد. برای مطابقت با هر سطح API، از 0 استفاده کنید. api را در @Device، در minApi و maxApi جایگزین می‌کند.

ویژگی‌های serial،‏ serials،‏ api، و apis برای هر آزمایشی که اجرا می‌شود اعمال می‌شود، بنابراین از گزینه --tests برای اجرای آزمایش‌هایی که به آن‌ها نیاز دارند استفاده کنید.

آزمایش بازگرداندن اعتبارنامه‌ها

اگر برنامه‌تان از بازگرداندن اعتبارنامه‌ها استفاده می‌کند، آزمایش کنید که کلید بازگرداندن آن پس‌از پشتیبان‌گیری و بازگرداندن باقی بماند. حالت‌های ابری و دستگاه‌به‌دستگاه کلید را با داده‌های برنامه‌تان پشتیبان‌گیری می‌کنند، اما LOCAL این کار را نمی‌کند.

کنش‌های دستگاه را بنویسید که کلید را ایجاد می‌کند و آن را با Credential Manager دریافت می‌کند. کنش ایجاد، شناسه اطلاعات کاربری کلید را برمی‌گرداند تا کنش درستی‌سنجی بتواند بررسی کند که بازیابی همان کلید را برمی‌گرداند. در این مثال، createRequestJson و getRequestJson رشته‌های JSON درخواست هستند که برنامه شما به Credential Manager ارسال می‌کند:

// 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) حاشیه‌نویسی کنید.

نوشتن آزمایش‌ها در جاوا

روش‌های BackupRestoreController توابع تعلیق Kotlin هستند. اگر آزمایش‌های میزبان را در جاوا می‌نویسید، از گونه 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 پایین‌تر از ۲۶.۳۳.۳۰ در دستگاه یا شبیه‌سازی باشد که سطح میانای برنامه‌سازی کاربردی ۳۶.۱ یا بالاتر را اجرا می‌کند. «خدمات Google Play» را ازطریق «فروشگاه Play» در دستگاه به‌روز کنید، یا از ایمیج سیستم همراه با نسخه ۲۶.۳۳.۳۰ یا بالاتر استفاده کنید.

مشکلات و محدودیت‌های شناخته‌شده

آزمایش پشتیبان‌گیری و بازیابی خودکار مشکلات و محدودیت‌های شناخته‌شده زیر را دارد:

  • پشتیبانی Android Studio از مجموعه‌های آزمون هنوز دردسترس نیست. ویرایشگر کد ممکن است خطاهای مرجع حل‌نشده را در فایل‌های منبع مجموعه آزمون نشان دهد، حتی زمانی که پروژه شما به‌درستی پیکربندی شده باشد. درعوض، آزمایش‌های پشتیبان‌گیری و بازیابی را از خط فرمان بااستفاده از Gradle بسازید و اجرا کنید.
  • آزمایش‌ها فقط در دستگاه‌ها و شبیه‌سازهایی اجرا می‌شوند که ازطریق adb به ماشین توسعه نرم‌افزار شما متصل هستند. از Firebase Test Lab پشتیبانی نمی‌شود.
  • آزمایش‌ها داده‌های برنامه‌تان را پاک می‌کنند و تنظیمات پشتیبان‌گیری را در دستگاه‌هایی که استفاده می‌کنند تغییر می‌دهند. توصیه می‌کنیم از دستگاه‌های آزمایشی اختصاصی استفاده کنید.

برای تغییرات در هر نسخه پخش، یادداشت‌های انتشار آزمایش پشتیبان‌گیری و بازیابی را ببینید.