اختبار الاحتفاظ بنسخة احتياطية من البيانات واستعادتها بشكل آلي

عندما يُعدّ المستخدمون جهازًا جديدًا أو يعيدون تثبيت تطبيقك، يمكن لنظام Android استعادة بيانات تطبيقك من نسخة احتياطية على السحابة الإلكترونية أو من خلال نقل البيانات من جهاز إلى آخر. إذا كانت قواعد الاحتفاظ بنسخة احتياطية تستبعد بيانات مهمة، أو إذا لم يتمكّن تطبيقك من استخدام البيانات التي يستعيدها، سيفقد المستخدمون إعداداتهم ومستوى تقدّمهم بدون ظهور أي خطأ.

تساعدك اختبارات الاحتفاظ بنسخة احتياطية والاستعادة التلقائية في رصد هذه المشاكل قبل إصدار تطبيقك. وهي اختبارات JUnit 5 يتم إجراؤها على جهاز التطوير وتتحكّم في الأجهزة المتصلة من خلال Android Debug Bridge (adb). يتم في كل اختبار تخزين بيانات التطبيق على الجهاز، والاحتفاظ بنسخة احتياطية من التطبيق واستعادتها من خلال إطار عمل الاحتفاظ بنسخة احتياطية من البيانات في جهاز Android، ثم التحقّق من سلامة البيانات. يمكنك ضبط الاختبارات كـ حزمة اختبارية للمكوّن الإضافي لنظام Gradle المتوافق مع Android ‏(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": يتم حفظ النسخة الاحتياطية على جهاز التطوير بدلاً من تحميلها أو إرسالها إلى جهاز آخر. يتطلّب وضعا السحابة الإلكترونية والنقل من جهاز إلى آخر توفُّر "خدمات Google Play" على الجهاز وإصدار قابل للتصحيح من تطبيقك. إذا كانت قواعد الاحتفاظ بنسخة احتياطية تعتمد على التشفير من جهة العميل، عليك اختبار وضعَي السحابة الإلكترونية.

المتطلبات

لاستخدام ميزة "اختبار الاحتفاظ بنسخة احتياطية والاستعادة تلقائيًا"، تأكَّد من أنّ بيئتك تستوفي المتطلبات التالية:

  • الإصدار 9.6.0 من Gradle أو إصدار أحدث
  • الإصدار 17 من JDK أو إصدار أحدث
  • جهاز أو محاكي يعمل بنظام التشغيل Android 12 (مستوى واجهة برمجة التطبيقات 31) أو إصدار أحدث ومتصل من خلال adb:
    • تتطلّب الأجهزة أو المحاكيات التي تعمل بالمستوى 36.1 من واجهة برمجة التطبيقات أو الإصدارات الأحدث استخدام الإصدار 26.33.30 من "خدمات Google Play" أو الإصدارات الأحدث.
    • لاختبار عمليات النقل بين جهازَين، عليك توصيلهما.

التوافق

يسرد الجدول التالي الحد الأدنى من الإصدارات والإصدارات المقترَحة لاختبار الاحتفاظ بنسخة احتياطية والاستعادة تلقائيًا:

المكوّن الإصدار الأدنى الإصدار المقترَح
استوديو Android Rabbit 2 Canary 1 الإصدار Rabbit 2 Canary 4 أو إصدار أحدث
المكوّن الإضافي لنظام Gradle المتوافق مع Android‏ (AGP) 9.5.0‎-alpha06 ‫9.5.0-alpha09 أو إصدار أحدث
مكتبتان (androidx.test.backup) 1.0.0‎-alpha01 الإصدار 1.0.0-alpha04 أو إصدار أحدث

يتطلّب كل إصدار مبكر حصري من "مكوّن Android الإضافي لنظام Gradle" الإصدار المبكر الحصري المطابق من "استوديو Android". لمزيد من المعلومات، يُرجى الاطّلاع على النُسخ الحصرية من "المكوّن الإضافي لنظام Gradle المتوافق مع Android".

إعداد مشروعك

لإضافة حزمة اختبارية للاحتفاظ بنسخة احتياطية والاستعادة إلى وحدة التطبيق، أكمِل الخطوات التالية:

تفعيل العلامات التجريبية

في ملف 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 التي يضيفها "مكوّن Android الإضافي في Gradle" إلى الحزمة، مثل "1.0.0-alpha04". هذه السمة اختيارية. وفي حال عدم تحديد الإصدار، يعود المكوّن الإضافي لنظام Gradle المتوافق مع Android إلى إصدار المكتبة التلقائي استنادًا إلى إصدار المكوّن الإضافي. تتطلّب الأمثلة الواردة في هذه الصفحة الإصدار 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.

إجراءات الكتابة على الجهاز

To seed or verify data that the built-in storage types don't cover, write device actions in the suite's androidTest source set. نفِّذ 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,
        )
    }
}

To pass values generated during seeding to the verify action, pass a map to 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 اختيار جهاز لكل مَعلمة عند إجراء الاختبارات. إذا تم توصيل جهاز واحد مطابق فقط، فستشارك كلتا المعلمتين هذا الجهاز وتحاكيان عملية النقل على جهاز واحد.

اختيار الأجهزة

By default, the test framework sorts online devices by serial number and gives each BackupRestoreController parameter its own matching device when enough devices are connected. أما المَعلمات التي لا يمكنها الحصول على جهاز منفصل، فتتم مشاركتها مع أول جهاز مطابق. لا يؤثر متغيّر البيئة 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 قائمة بمستويات واجهة برمجة التطبيقات مفصولة بفواصل، ويتم تخصيصها لمَعلمات الجهاز بالترتيب. استخدِم 0 لمطابقة أي مستوى من واجهة برمجة التطبيقات. يحلّ محلّ "api" في ‎@Device، ضمن "minApi" و"maxApi".
androidx.test.backup.device.api مستوى واجهة برمجة التطبيقات لمَعلمات الجهاز التي لا يغطيها apis. استخدِم 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 تم تفعيل ميزة الاحتفاظ بنسخة احتياطية فيه وقفل شاشة. بخلاف ذلك، يعرض Credential Manager الخطأ E2eeUnavailableException.

على عكس عملية الاستعادة من السحابة الإلكترونية على جهاز جديد، لا تتطلّب أوضاع السحابة الإلكترونية استخدام حساب Google نفسه أو قفل شاشة الجهاز المصدر، وتستعيد المفاتيح التي تم إنشاؤها مع ضبط isCloudBackupEnabled على false.

الاحتفاظ ببيانات التطبيق بين الاختبارات

قبل كل اختبار، يمحو إطار عمل الاختبار بيانات تطبيقك على كل جهاز يستخدمه الاختبار. لإيقاف هذه الميزة لفئة أو طريقة اختبار، يمكنك إضافة التعليق التوضيحي @Isolation(IsolationPolicy.MANUAL) إليها.

كتابة الاختبارات بلغة Java

طُرق BackupRestoreController هي دوال تعليق في Kotlin. إذا كنت تكتب اختبارات المضيف بلغة Java، استخدِم الصيغة Async لكل طريقة، مثل runBackupRestoreFlowAsync، التي تعرض ListenableFuture.

إجراء اختبارات النسخ الاحتياطي والاستعادة

ينشئ "مكوّن Android الإضافي لنظام Gradle" مهمة Gradle لكل صيغة تختبرها مجموعة الاختبارات، وتحمل الاسم test{Variant}{SuiteName}DefaultTestSuite. لتشغيل حزمة backupTest على صيغة debug، عليك توصيل أجهزتك وتشغيل المهمة:

./gradlew :app:testDebugBackupTestDefaultTestSuite

لا تنفّذ المهام check وtest وconnectedCheck الحزم الاختبارية للنسخ الاحتياطي والاستعادة، لذا عليك تنفيذ مهمة الحزمة بشكل صريح.

لتشغيل فئة أو طريقة اختبار واحدة، استخدِم الخيار --tests:

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

فحص تقارير الاختبار

بعد اكتمال عملية الاختبار، ينشئ مكوّن Android Gradle الإضافي تقرير اختبار 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 على جهاز أو محاكي يعمل بالمستوى 36.1 أو أعلى من واجهة برمجة التطبيقات. يُرجى تحديث "خدمات Google Play" من خلال "متجر Play" على الجهاز، أو استخدام نسخة نظام تتضمّن الإصدار 26.33.30 أو إصدارًا أحدث.

الحدود والمشاكل المعروفة

في ما يلي المشاكل والقيود المعروفة في اختبارات الاحتفاظ بنسخة احتياطية واستعادتها تلقائيًا:

  • لا تتوفّر حتى الآن إمكانية استخدام الحِزم الاختبارية في "استوديو Android". قد تعرض أداة تعديل الرموز أخطاء مراجع لم يتم حلّها في ملفات مصدر الحزمة الاختبارية حتى إذا تم ضبط مشروعك بشكل صحيح. يمكنك إنشاء اختبارات النسخ الاحتياطي والاستعادة وتشغيلها من سطر الأوامر باستخدام Gradle بدلاً من ذلك.
  • يتم إجراء الاختبارات فقط على الأجهزة والمحاكيات المتصلة بجهاز التطوير من خلال adb. لا يتوفّر "مختبر Firebase الافتراضي".
  • تؤدي الاختبارات إلى محو بيانات تطبيقك وتغيير إعدادات الاحتفاظ بنسخة احتياطية على الأجهزة التي تستخدمها. ننصحك باستخدام أجهزة اختبار مخصّصة.

للاطّلاع على التغييرات في كل إصدار، يُرجى مراجعة ملاحظات الإصدار الخاصة باختبار ميزة "الاحتفاظ بنسخة احتياطية والاستعادة".