כשמשתמשים מגדירים מכשיר חדש או מתקינים מחדש את האפליקציה, מערכת Android יכולה לשחזר את נתוני האפליקציה מגיבוי בענן או מהעברה ממכשיר למכשיר. אם כללי הגיבוי לא כוללים נתונים חשובים, או שהאפליקציה לא יכולה להשתמש בנתונים שהיא משחזרת, המשתמשים מאבדים את ההגדרות ואת ההתקדמות שלהם בלי לראות שגיאה.
בדיקות אוטומטיות של גיבוי ושחזור עוזרות לכם לזהות את הבעיות האלה לפני שאתם משיקים את האפליקציה. אלה בדיקות JUnit 5 שפועלות במחשב הפיתוח ושולטות במכשירים מחוברים דרך Android Debug Bridge (adb). כל בדיקה מזינה נתוני אפליקציה במכשיר, מגבה ומשחזרת את האפליקציה דרך מסגרת הגיבוי של Android, ואז בודקת שהנתונים תקינים. אתם מגדירים את הבדיקות כחבילת מקרים לבדיקה של פלאגין של Android Gradle (AGP) בקובץ ה-build של המודול.
איך זה עובד
חבילת מקרים לבדיקה של גיבוי ושחזור מורכבת משני חלקים שפועלים יחד:
- בדיקות מארח הן בדיקות 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 Services במכשיר ולהשתמש בגרסה של האפליקציה שניתן לנפות בה באגים. אם כללי הגיבוי שלכם תלויים בהצפנה בצד הלקוח, כדאי לבדוק את שני מצבי הענן.
דרישות
כדי להשתמש בבדיקות אוטומטיות של גיבוי ושחזור, צריך לוודא שהסביבה עומדת בדרישות הבאות:
- Gradle מגרסה 9.6.0 ואילך.
- JDK מגרסה 17 ואילך.
- מכשיר או אמולטור עם Android 12 (רמת API 31) ומעלה שמחובר דרך adb:
- במכשירים או באמולטורים עם רמת API 36.1 ומעלה, נדרשת גרסה 26.33.30 ומעלה של Google Play Services.
- כדי לבדוק העברות בין שני מכשירים, צריך לחבר שני מכשירים.
תאימות
בטבלה הבאה מפורטות הגרסאות המינימליות והמומלצות לבדיקת גיבוי ושחזור אוטומטיים:
| רכיב | גרסת המינימום | הגרסה המומלצת |
|---|---|---|
| Android Studio | ארנב 2 קנרית 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-alpha03 ואילך |
כל גרסת טרום-השקה של AGP דורשת את גרסת טרום-ההשקה התואמת של Android Studio. מידע נוסף זמין במאמר גרסאות טרום-השקה (Preview) של AGP.
הגדרת הפרויקט
כדי להוסיף חבילת מקרים לבדיקה של גיבוי ושחזור למודול של האפליקציה, מבצעים את הפעולות הבאות:
הפעלת דגלים ניסיוניים
בקובץ gradle.properties של הפרויקט, מפעילים את חבילות המקרים לבדיקה ואת בדיקות הגיבוי:
android.experimental.testSuiteSupport=true
android.experimental.enableBackupTest=true
הצהרה על חבילת המקרים לבדיקה
בקובץ build.gradle.kts של מודול האפליקציה, מצהירים על חבילת מקרים לבדיקה של הגיבוי בבלוק testOptions:
android {
testOptions {
backupTests {
create("backupTest") {
backupTestLibraryVersion = "1.0.0-alpha03"
}
}
}
}
שם החבילה, backupTest בדוגמה הזו, קובע את ספריות קבוצת המקורות ואת שמות המשימות של Gradle. אתם יכולים להתאים אישית את החבילה באמצעות המאפיינים האופציונליים הבאים:
- משתמשים ב-
backupTestLibraryVersionכדי להגדיר את הגרסה של ספריותandroidx.test.backupש-AGP מוסיף לחבילה, כמו"1.0.0-alpha03". המאפיין הזה הוא אופציונלי. אם לא תציינו גרסה, AGP ישתמש בגרסת ברירת המחדל של הספרייה בהתאם לגרסת ה-AGP. הדוגמאות בדף הזה דורשות גרסה 1.0.0-alpha03 ואילך. - משתמשים ב-
targetVariantsכדי לציין את וריאציות ה-build שהחבילה בודקת, כמו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 עם הנתונים להזנה, ספרייה לקובץ הגיבוי ומצב העברה. השיטה מריצה את השלבים הבאים במכשיר:
- מזין נתונים לכל דומיין אחסון.
- האפליקציה תיעצר.
- מגבה את האפליקציה.
- מנקה את נתוני האפליקציה.
- משחזר את הגיבוי.
- מוודא שלכל דומיין אחסון יש את ערך הבסיס.
אם שלב מסוים נכשל, השיטה זורקת חריג, והבדיקה נכשלת. בדוגמה הבאה נבדקים מפתח SharedPreferences, שורה ב-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 = "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, מומלץ לבדוק את תוכן מסד הנתונים באמצעות פעולות במכשיר שמשתמשות באובייקטי ה-DAO, כדי שלמסד הנתונים תהיה הסכימה ש-Room מצפה לה.
פעולות כתיבה במכשיר
כדי להזין או לאמת נתונים שלא נכללים בסוגי האחסון המובנים, כותבים פעולות במכשיר בקבוצת המקורות androidTest של החבילה. פעולה במכשיר מטמיעה את BackupDeviceAction ומבטלת שני איברים:
- מגדירים את
phaseלערךBackupDeviceAction.PHASE_POPULATEלפעולה שמאכלסת נתונים, או לערךBackupDeviceAction.PHASE_VERIFYלפעולה שבודקת נתונים. - מטמיעים את
executeכדי להריץ את הפעולה עםContextשל האפליקציה והארגומנטים מבדיקת המארח. מחזירים תוצאה שנוצרה באמצעות השיטהBackupDeviceActionResult.successאוBackupDeviceActionResult.failure, ומעבירים הודעת שגיאה אלfailure.
השיטה execute פועלת בתהליך של האפליקציה בשרשור ברקע, כך שהיא יכולה לחסום עד לסיום פעולות הקריאה והכתיבה.
בדוגמה הבאה, מאתחלים ומאמתים הגדרת עיצוב שהאפליקציה שומרת באמצעות Preferences DataStore. האפליקציה מגדירה את DataStore בקבוצת המקורות הראשית שלה:
// In app/src/main/kotlin/com/example/myapp/Settings.kt
package com.example.myapp
import android.content.Context
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
val Context.settingsDataStore by preferencesDataStore(name = "settings")
val THEME_KEY = stringPreferencesKey("theme")
הפעולות במכשיר משתמשות באותן הגדרות:
// In app/src/backupTest/androidTest/kotlin/com/example/myapp/ThemeActions.kt
package com.example.myapp
import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.test.backup.BackupDeviceAction
import androidx.test.backup.BackupDeviceActionArgs
import androidx.test.backup.BackupDeviceActionResult
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
class SeedThemeAction : BackupDeviceAction {
override val phase = BackupDeviceAction.PHASE_POPULATE
override fun execute(
context: Context,
args: BackupDeviceActionArgs,
): BackupDeviceActionResult {
val theme = args["theme"]
?: return BackupDeviceActionResult.failure("Missing theme argument")
runBlocking {
context.settingsDataStore.edit { it[THEME_KEY] = theme }
}
return BackupDeviceActionResult.success()
}
}
class VerifyThemeAction : BackupDeviceAction {
override val phase = BackupDeviceAction.PHASE_VERIFY
override fun execute(
context: Context,
args: BackupDeviceActionArgs,
): BackupDeviceActionResult {
val expected = args["theme"]
val actual = runBlocking {
context.settingsDataStore.data.first()[THEME_KEY]
}
return if (actual == expected) {
BackupDeviceActionResult.success()
} else {
BackupDeviceActionResult.failure("Expected $expected, found $actual")
}
}
}
כדי להחזיר נתונים לבדיקה המארחת, מעבירים מפה ל-method success. בדיקת המארח קוראת את המפה מ-BackupActionResult.Success.data.
מריצים כל שלב בתהליך
כדי להשתמש בפעולות במכשיר או להתאים אישית את התהליך, צריך להפעיל כל שלב בעצמכם. בדיקת המארח הבאה מריצה את פעולות המכשיר מהקטע הקודם סביב גיבוי ושחזור בענן:
package com.example.myapp
import androidx.test.backup.host.BackupActionResult
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)
class ThemeBackupTest {
@Test
fun themeSurvivesCloudBackup(
device: BackupRestoreController,
@TempDir outputDir: Path,
): Unit = runBlocking {
val args = mapOf("theme" to "dark")
device.runOnDevice(SEED_THEME, args).assertSuccess()
device.stopApp()
val backupFile = device.performBackup(
BackupTransportMode.CLOUD_ENCRYPTED,
outputDir,
)
device.clearAppData()
device.performRestore(backupFile)
device.runOnDevice(VERIFY_THEME, args).assertSuccess()
}
}
fun BackupActionResult.assertSuccess() {
if (this is BackupActionResult.Failure) fail(errorMessage)
}
אם פעולה במכשיר נכשלת, runOnDevice מחזיר BackupActionResult.Failure במקום להציג חריגה, אז צריך לבדוק את התוצאה. אם השלבים האחרים נכשלים, הם גורמים לחריגה.
בדיקת העברות בין מכשירים
כדי לבדוק העברה בין שני מכשירים, צריך להצהיר על פרמטר BackupRestoreController לכל מכשיר. מגבים את האפליקציה במכשיר המקור, ואז משחזרים את קובץ הגיבוי במכשיר היעד. בדוגמה הבאה נעשה שימוש חוזר בקבועים ובפונקציית העזר assertSuccess מהדוגמה הקודמת:
package com.example.myapp
import androidx.test.backup.host.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)
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 מאפשרים לכם לבחור מכשיר לכל פרמטר כשאתם מריצים את הבדיקות. אם רק מכשיר אחד מחובר, שני הפרמטרים משתמשים במכשיר הזה, והבדיקה מדמה את ההעברה במכשיר אחד. ניקוי הנתונים של היעד מבטיח שהנתונים שישוחזרו יגיעו מהגיבוי.
בחירת מכשירים
כברירת מחדל, מסגרת הבדיקה ממיינת את המכשירים המחוברים לפי מספר סידורי ומקצה אותם לפרמטרים של 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הקישור חל רק על פרמטרים עם התפקיד הזה. פרמטרים אחרים מקבלים את שאר המכשירים.
אפשר גם לבחור מכשירים עם הערכים @Device של serial ו-api, או עם מאפייני המערכת הבאים, שמקבלים קדימות על פני הערכים של @Device:
| מאפיין מערכת | תיאור |
|---|---|
androidx.test.backup.device.serial.{role} |
המספר הסידורי של המכשיר לפרמטרים שמסומנים ב-@Device(role = "{role}"). היא מקבלת עדיפות על פני serials ו-serial. |
androidx.test.backup.device.serials |
רשימה מופרדת בפסיקים של מספרים סידוריים, שמוקצים לפרמטרים של המכשיר לפי הסדר. |
androidx.test.backup.device.serial |
המספר הסידורי של המכשיר, לפרמטרים של המכשיר שלא נכללים ב-serials. |
androidx.test.backup.device.apis |
רשימה מופרדת בפסיקים של רמות API, שמוקצות לפרמטרים של המכשיר לפי הסדר. |
androidx.test.backup.device.api |
רמת ה-API של המכשיר לפרמטרים של המכשיר שלא נכללים ב-apis. |
מעבירים את המאפיינים האלה עם -D באותו אופן. המאפיינים 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 val phase = BackupDeviceAction.PHASE_POPULATE
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 val phase = BackupDeviceAction.PHASE_VERIFY
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")
}
}
}
בבדיקה הבאה של המארח, המפתח נוצר במכשיר המקור, משוחזר במכשיר היעד ונבדק שם. הוא משתמש בפונקציית העזר assertSuccess מהקטע הרצת כל שלב בתהליך:
package com.example.myapp
import androidx.test.backup.host.BackupActionResult
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
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 {
val created = source.runOnDevice(CREATE_RESTORE_KEY)
created.assertSuccess()
val credentialId =
(created as BackupActionResult.Success).data.getValue("credential_id")
source.stopApp()
val backupFile = source.performBackup(
BackupTransportMode.DEVICE_TO_DEVICE,
outputDir,
)
// Also deletes any restore key on the target.
target.clearAppData()
target.performRestore(backupFile)
target.runOnDevice(VERIFY_RESTORE_KEY, mapOf("credential_id" to credentialId))
.assertSuccess()
}
}
כדי ליצור מפתח כשהערך של isCloudBackupEnabled מוגדר כ-true (ברירת המחדל), במכשיר המקור צריך להיות חשבון Google עם גיבוי מופעל ונעילת מסך.
אחרת, Credential Manager יציג את השגיאה E2eeUnavailableException.
בניגוד לשחזור מהענן במכשיר חדש, במצבי הענן לא נדרש אותו חשבון Google או אותה שיטה לביטול נעילת המסך של מכשיר המקור, והם משחזרים מפתחות שנוצרו כש-isCloudBackupEnabled מוגדר ל-false.
שמירת נתוני האפליקציה בין בדיקות
לפני כל בדיקה, מסגרת הבדיקה מוחקת את נתוני האפליקציה בכל מכשיר שבו מתבצעת הבדיקה. כדי להשבית את האפשרות הזו למחלקה או לשיטה של בדיקה, מוסיפים לה את ההערה @Isolation(IsolationPolicy.MANUAL).
כתיבת בדיקות ב-Java
השיטות של BackupRestoreController הן פונקציות השעיה של Kotlin. אם אתם כותבים בדיקות מארח ב-Java, השתמשו בווריאנט Async של כל שיטה, כמו runBackupRestoreFlowAsync, שמחזירה ListenableFuture.
הפעלת בדיקות של גיבוי ושחזור
ה-AGP יוצר משימת Gradle לכל וריאנט שחבילת הבדיקות בודקת, בשם test{Variant}{SuiteName}DefaultTestSuite. כדי להריץ את חבילת backupTest בגרסה debug, מחברים את המכשירים ומריצים את המשימה:
./gradlew :app:testDebugBackupTestDefaultTestSuiteהמשימות check, test ו-connectedCheck לא מריצות חבילות מקרים לבדיקה של גיבוי ושחזור, אז צריך להריץ את המשימה של החבילה באופן מפורש.
כדי להריץ מחלקה או שיטה של בדיקה אחת, משתמשים באפשרות --tests:
./gradlew :app:testDebugBackupTestDefaultTestSuite --tests "com.example.myapp.ThemeBackupTest"בדיקת דוחות הבדיקה
אחרי שהרצת הבדיקה מסתיימת, AGP יוצר דוח בדיקה בפורמט HTML בנתיב {module}/build/reports/tests/{taskName}/index.html, למשל app/build/reports/tests/testDebugBackupTestDefaultTestSuite/index.html.
פתרון בעיות בשחזורים שנכשלו
שחזור מהענן או ממכשיר למכשיר נכשל ומוצגת הודעת שגיאה במקרים הבאים:
- Google Play Services מדלגים על האפליקציה, למשל כי מזהה האפליקציה מתחיל ב-
com.google.או ב-com.android., הגיבוי שלה ריק או שהיא מגדירה אתandroid:allowBackup="false". כדי לבדוק אפליקציה עם מזהה אפליקציה כזה, צריך להשתמש במצבLOCAL. - גרסת Google Play Services נמוכה מ-26.33.30 במכשיר או באמולטור שפועלים ברמת API 36.1 ומעלה. צריך לעדכן את Google Play Services דרך חנות Play במכשיר, או להשתמש בקובץ אימג' של המערכת שמגיע עם גרסה 26.33.30 ואילך.
בעיות ומגבלות ידועות
אלה הבעיות והמגבלות הידועות בבדיקות אוטומטיות של גיבוי ושחזור:
- עדיין אין תמיכה ב-Android Studio בחבילות מקרים לבדיקה. יכול להיות שבעורך הקוד יוצגו שגיאות של הפניות לא פתורות בקובצי המקור של חבילת המקרים לבדיקה, גם אם הפרויקט מוגדר בצורה נכונה. במקום זאת, אפשר ליצור ולהריץ בדיקות גיבוי ושחזור משורת הפקודה באמצעות Gradle.
- הבדיקות פועלות רק במכשירים ובאמולטורים שמחוברים למחשב הפיתוח באמצעות adb. אי אפשר להשתמש ב-Firebase Test Lab.
- הבדיקות מוחקות את נתוני האפליקציה ומשנות את הגדרות הגיבוי במכשירים שבהם הן משתמשות. מומלץ להשתמש במכשירי בדיקה ייעודיים.
בנתוני הגרסה של בדיקת הגיבוי והשחזור מפורטים השינויים בכל גרסה.