ה-codelab הזה הוא חלק מהקורס Advanced Android in Kotlin (פיתוח מתקדם ל-Android ב-Kotlin). כדי להפיק את המרב מהקורס הזה, מומלץ לעבוד על ה-codelabs לפי הסדר, אבל זה לא חובה. כל ה-codelab של הקורס מפורטים בדף הנחיתה של ה-codelab בנושא Android מתקדם ב-Kotlin.
מבוא
בסדנת ה-codelab השנייה בנושא בדיקות נתמקד בבדיקות כפולות: מתי כדאי להשתמש בהן ב-Android ואיך להטמיע אותן באמצעות הזרקת תלות, תבנית Service Locator וספריות. במהלך התהליך הזה תלמדו איך לכתוב:
- בדיקות יחידה במאגר
- בדיקות שילוב של קטעים ושל ViewModel
- בדיקות ניווט במקטעים
מה שכדאי לדעת
חשוב שתכירו את:
- שפת התכנות Kotlin
- מושגי הבדיקה שמוסברים ב-codelab הראשון: כתיחה והרצה של בדיקות יחידה ב-Android, באמצעות JUnit, Hamcrest, AndroidX test, Robolectric, וגם בדיקה של LiveData
- ספריות הליבה הבאות של Android Jetpack:
ViewModel, LiveDataורכיב הניווט - ארכיטקטורת האפליקציה, בהתאם לתבנית מהמדריך לארכיטקטורת אפליקציות ומסדנאות ה-codelab בנושא יסודות Android
- היסודות של קורוטינות ב-Android
מה תלמדו
- איך מתכננים אסטרטגיית בדיקה
- איך ליצור כפילים לבדיקה ולהשתמש בהם, כלומר, זיופים ומוקים
- איך משתמשים בהזרקת תלות ידנית ב-Android לבדיקות יחידה ובדיקות שילוב
- איך משתמשים בתבנית Service Locator
- איך בודקים מאגרי מידע, קטעים, מודלים של תצוגות ורכיב הניווט
תשתמשו בספריות ובמושגי הקוד הבאים:
- הפקודה
runBlockingוהפקודהrunBlockingTest FragmentScenario- Espresso
- Mockito
הפעולות שתבצעו:
- כתיבת בדיקות יחידה למאגר באמצעות כפיל בדיקה והזרקת תלות.
- כתיבת בדיקות יחידה למודל תצוגה באמצעות כפיל בדיקה והזרקת תלות.
- כתיבת בדיקות שילוב עבור פרגמנטים ומודלים של תצוגות שלהם באמצעות מסגרת הבדיקות של ממשק המשתמש Espresso.
- כתיבת בדיקות ניווט באמצעות Mockito ו-Espresso.
בסדרת ה-codelabs הזו, תעבדו עם אפליקציית TO-DO Notes. האפליקציה מאפשרת לכם לרשום משימות לביצוע ולהציג אותן ברשימה. אחר כך תוכלו לסמן אותן כהשלמה או כלא השלמה, לסנן אותן או למחוק אותן.

האפליקציה הזו כתובה ב-Kotlin, יש לה כמה מסכים, היא משתמשת ברכיבי Jetpack ומבוססת על הארכיטקטורה שמופיעה במדריך לארכיטקטורת אפליקציות. אם תלמדו איך לבדוק את האפליקציה הזו, תוכלו לבדוק גם אפליקציות שמשתמשות באותן ספריות ובאותה ארכיטקטורה.
הורדת הקוד
כדי להתחיל, מורידים את הקוד:
אפשרות אחרת היא לשכפל את מאגר GitHub של הקוד:
$ git clone https://github.com/googlecodelabs/android-testing.git $ cd android-testing $ git checkout end_codelab_1
חשוב להקדיש רגע כדי להבין את הקוד, ולפעול לפי ההוראות שבהמשך.
שלב 1: מריצים את אפליקציית הדוגמה
אחרי שמורידים את אפליקציית רשימת המשימות, פותחים אותה ב-Android Studio ומריצים אותה. הקוד אמור לעבור קומפילציה. כדי לבדוק את האפליקציה:
- יוצרים משימה חדשה באמצעות לחצן הפעולה הצף עם סמל הפלוס. קודם נותנים שם למשימה, ואז מזינים מידע נוסף עליה. שומרים אותו באמצעות לחצן הפעולה הצף עם סימן הווי הירוק.
- ברשימת המשימות, לוחצים על שם המשימה שהושלמה וצופים במסך הפרטים של המשימה כדי לראות את שאר התיאור.
- ברשימה או במסך הפרטים, מסמנים את תיבת הסימון של המשימה כדי לשנות את הסטטוס שלה להושלמה.
- חוזרים למסך המשימות, פותחים את תפריט הסינון ומסננים את המשימות לפי הסטטוס פעיל והושלם.
- פותחים את חלונית ההזזה לניווט ולוחצים על סטטיסטיקות.
- חוזרים למסך הסקירה הכללית, ובתפריט מגירת הניווט בוחרים באפשרות מחיקת המשימות שבוצעו כדי למחוק את כל המשימות עם הסטטוס בוצעה.
שלב 2: בודקים את קוד האפליקציה לדוגמה
אפליקציית רשימת המשימות מבוססת על דגימת הבדיקה והארכיטקטורה של Architecture Blueprints (באמצעות גרסת הארכיטקטורה התגובתית של הדגימה). האפליקציה פועלת לפי הארכיטקטורה שמוסברת במדריך לארכיטקטורת אפליקציות. הוא משתמש ב-ViewModels עם Fragments, במאגר וב-Room. אם אתם מכירים את אחת מהדוגמאות שבהמשך, האפליקציה הזו מבוססת על ארכיטקטורה דומה:
- Room with a View Codelab
- Android Kotlin Fundamentals training codelabs
- Codelabs מתקדמים בנושא Android
- Android Sunflower Sample
- קורס ההדרכה של Udacity בנושא פיתוח אפליקציות ל-Android באמצעות Kotlin
חשוב יותר להבין את הארכיטקטורה הכללית של האפליקציה מאשר להבין לעומק את הלוגיקה בכל אחת מהשכבות.
סיכום החבילות שזמינות:
חבילה: | |
| מסך הוספה או עריכה של משימה: קוד שכבת ממשק המשתמש להוספה או לעריכה של משימה. |
| שכבת הנתונים: השכבה הזו עוסקת בשכבת הנתונים של המשימות. הוא מכיל את מסד הנתונים, הרשת וקוד המאגר. |
| מסך הנתונים הסטטיסטיים: קוד שכבת ממשק המשתמש של מסך הנתונים הסטטיסטיים. |
| מסך פרטי המשימה: קוד שכבת ממשק המשתמש של משימה אחת. |
| מסך המשימות: קוד שכבת ממשק המשתמש לרשימה של כל המשימות. |
| מחלקות כלי עזר: מחלקות משותפות שמשמשות בחלקים שונים של האפליקציה, למשל עבור פריסת הרענון בהחלקה שמשמשת בכמה מסכים. |
שכבת נתונים (.data)
האפליקציה הזו כוללת שכבת רשת מדומה בחבילה remote ושכבת מסד נתונים בחבילה local. כדי לפשט את הדברים, בפרויקט הזה שכבת הרשת מדומה באמצעות HashMap עם השהיה, במקום לבצע בקשות רשת אמיתיות.
השכבה DefaultTasksRepository מתאמת או מתווכת בין שכבת הרשת לשכבת מסד הנתונים, והיא זו שמחזירה נתונים לשכבת ממשק המשתמש.
שכבת ממשק המשתמש (.addedittask, .statistics, .taskdetail, .tasks)
כל אחת מהחבילות של שכבת ממשק המשתמש מכילה קטע ו-ViewModel, וגם כל מחלקה אחרת שנדרשת לממשק המשתמש (כמו מתאם לרשימת המשימות). TaskActivity היא הפעילות שמכילה את כל המקטעים.
ניווט
הניווט באפליקציה נשלט על ידי רכיב הניווט. הוא מוגדר בקובץ nav_graph.xml. הניווט מופעל במודלים של התצוגה באמצעות המחלקה Event. המודלים של התצוגה גם קובעים אילו ארגומנטים להעביר. הקטעים עוקבים אחרי ה-Event ומבצעים את הניווט בפועל בין המסכים.
ב-codelab הזה תלמדו איך לבדוק מאגרי מידע, להציג מודלים וקטעים באמצעות כפילים לבדיקה והזרקת תלות. לפני שמתחילים להסביר מהן הבדיקות האלה, חשוב להבין את ההיגיון שינחה אתכם לגבי מה לכתוב ואיך לכתוב את הבדיקות האלה.
בקטע הזה נסביר על כמה שיטות מומלצות לבדיקות באופן כללי, כפי שהן חלות על Android.
פירמידת הבדיקות
כשחושבים על אסטרטגיית בדיקות, יש שלושה היבטים קשורים של בדיקות:
- היקף – כמה מהקוד נבדק? אפשר להריץ בדיקות על שיטה אחת, על האפליקציה כולה או על חלק ממנה.
- מהירות – באיזו מהירות הבדיקה מופעלת? מהירויות הבדיקה יכולות לנוע בין אלפיות השנייה לכמה דקות.
- נאמנות למציאות – עד כמה הבדיקה משקפת את המציאות? לדוגמה, אם חלק מהקוד שאתם בודקים צריך לשלוח בקשה לרשת, האם קוד הבדיקה באמת שולח את הבקשה הזו לרשת או שהוא מזייף את התוצאה? אם הבדיקה מתקשרת בפועל עם הרשת, המשמעות היא שהיא מדויקת יותר. החיסרון הוא שהבדיקה עלולה להימשך זמן רב יותר, עלולה לגרום לשגיאות אם הרשת מושבתת או עלולה להיות יקרה לשימוש.
יש פה פשרה מובנית בין ההיבטים האלה. לדוגמה, יש פשרה בין מהירות לבין נאמנות – ככל שהבדיקה מהירה יותר, בדרך כלל הנאמנות נמוכה יותר, ולהפך. אחת הדרכים הנפוצות לחלק בדיקות אוטומטיות היא לשלוש הקטגוריות הבאות:
- בדיקות יחידה – אלה בדיקות ממוקדות מאוד שמופעלות על מחלקה אחת, בדרך כלל על שיטה אחת במחלקה הזו. אם בדיקת יחידה נכשלת, אפשר לדעת בדיוק איפה הבעיה בקוד. רמת הדיוק שלהן נמוכה כי בעולם האמיתי, האפליקציה כוללת הרבה יותר מהרצה של שיטה או מחלקה אחת. הם מהירים מספיק כדי לפעול בכל פעם שמשנים את הקוד. ברוב המקרים אלה יהיו בדיקות שמופעלות באופן מקומי (ב
testsource set). דוגמה: בדיקה של שיטות יחידות במאגרי תצוגות ובמאגרים. - בדיקות שילוב – הבדיקות האלה בודקות את האינטראקציה בין כמה מחלקות כדי לוודא שהן מתנהגות כמצופה כשמשתמשים בהן יחד. אחת הדרכים לבנות בדיקות שילוב היא לבדוק תכונה אחת, כמו היכולת לשמור משימה. הן בודקות היקף קוד גדול יותר מבדיקות יחידה, אבל הן עדיין מותאמות לפעולה מהירה, לעומת נאמנות מלאה. אפשר להריץ אותם באופן מקומי או כבדיקות מכשור, בהתאם למצב. דוגמה: בדיקת כל הפונקציונליות של צמד יחיד של fragment ו-view model.
- בדיקות מקצה לקצה (E2e) – בדיקה של שילוב בין תכונות שפועלות יחד. הם בודקים חלקים גדולים של האפליקציה, מדמים שימוש אמיתי בצורה מדויקת ולכן בדרך כלל הם איטיים. הם הכי מדויקים ומראים שהאפליקציה שלכם באמת פועלת כמכלול. בדרך כלל, הבדיקות האלה יהיו בדיקות עם מכשור (ב
androidTestערכת המקור)
דוגמה: הפעלה של האפליקציה כולה ובדיקה של כמה תכונות ביחד.
היחס המומלץ בין הבדיקות האלה מוצג בדרך כלל בצורת פירמידה, כאשר הרוב המכריע של הבדיקות הן בדיקות יחידה.

ארכיטקטורה ובדיקות
היכולת שלכם לבדוק את האפליקציה בכל הרמות השונות של פירמידת הבדיקות קשורה באופן מובנה לארכיטקטורה של האפליקציה. לדוגמה, באפליקציה עם ארכיטקטורה גרועה במיוחד, יכול להיות שכל הלוגיקה נמצאת בתוך שיטה אחת. אולי תוכלו לכתוב בדיקה מקצה לקצה, כי הבדיקות האלה בדרך כלל בודקות חלקים גדולים של האפליקציה, אבל מה לגבי כתיבת בדיקות יחידה או בדיקות שילוב? כשכל הקוד נמצא במקום אחד, קשה לבדוק רק את הקוד שקשור ליחידה או לתכונה מסוימת.
גישה טובה יותר היא לחלק את הלוגיקה של האפליקציה לכמה שיטות וכיתות, כדי שיהיה אפשר לבדוק כל חלק בנפרד. ארכיטקטורה היא דרך לחלק ולארגן את הקוד, וכך לבצע בדיקות יחידה ובדיקות שילוב בקלות רבה יותר. אפליקציית רשימת המטלות שתיבדק פועלת לפי ארכיטקטורה מסוימת:
בשיעור הזה נראה איך לבדוק חלקים מהארכיטקטורה שלמעלה, בבידוד מתאים:
- קודם כול, תבצעו בדיקת יחידה של מאגר.
- לאחר מכן תשתמשו ב-test double במודל התצוגה, שנדרש לבדיקת יחידות ולבדיקת שילוב של מודל התצוגה.
- בשלב הבא תלמדו לכתוב בדיקות שילוב לקטעי קוד ולמודלים של תצוגות.
- בסוף, תלמדו לכתוב בדיקות אינטגרציה שכוללות את רכיב הניווט.
בשיעור הבא נסביר על בדיקות מקצה לקצה.
כשכותבים בדיקת יחידה לחלק ממחלקה (שיטה או אוסף קטן של שיטות), המטרה היא לבדוק רק את הקוד במחלקה הזו.
יכול להיות שיהיה לכם קשה לבדוק רק קוד בכיתה ספציפית או בכיתות ספציפיות. נתבונן בדוגמה. פותחים את הכיתה data.source.DefaultTaskRepository במערך המקור main. זהו המאגר של האפליקציה, והוא המחלקה שבה תכתבו את בדיקות היחידה הבאות.
המטרה היא לבדוק רק את הקוד בכיתה הזו. עם זאת, הפונקציה DefaultTaskRepository תלויה בפונקציות אחרות, כמו LocalTaskDataSource ו-RemoteTaskDataSource, כדי לפעול. אפשר גם לומר ש-LocalTaskDataSource ו-RemoteTaskDataSource הם תלויות של DefaultTaskRepository.
לכן, כל שיטה ב-DefaultTaskRepository קוראת לשיטות במחלקות של מקורות נתונים, שבתורן קוראות לשיטות במחלקות אחרות כדי לשמור מידע במסד נתונים או לתקשר עם הרשת.

לדוגמה, אפשר לעיין בשיטה הזו ב-DefaultTasksRepo.
suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>> {
if (forceUpdate) {
try {
updateTasksFromRemoteDataSource()
} catch (ex: Exception) {
return Result.Error(ex)
}
}
return tasksLocalDataSource.getTasks()
}getTasks היא אחת מהקריאות ה "בסיסיות" ביותר שאפשר לבצע למאגר. השיטה הזו כוללת קריאה ממסד נתונים של SQLite וביצוע קריאות לרשת (הקריאה אל updateTasksFromRemoteDataSource). היא כוללת הרבה יותר קוד מאשר רק קוד המאגר.
הנה כמה סיבות ספציפיות לכך שקשה לבדוק את המאגר:
- כדי לבצע אפילו את הבדיקות הפשוטות ביותר במאגר הזה, צריך לחשוב על יצירה וניהול של מסד נתונים. זה מעלה שאלות כמו "האם זו צריכה להיות בדיקה מקומית או בדיקה עם מכשור?" והאם כדאי להשתמש ב-AndroidX Test כדי לקבל סביבת Android מדומה.
- חלקים מסוימים בקוד, כמו קוד הרשת, יכולים לפעול במשך זמן רב, או אפילו להיכשל מדי פעם, וכך ליצור בדיקות ארוכות ולא יציבות.
- הבדיקות שלכם עלולות לאבד את היכולת לאבחן איזה קוד אחראי לכשל בבדיקה. הבדיקות שלכם יכולות להתחיל לבדוק קוד שלא נמצא במאגר, ולכן, לדוגמה, בדיקות היחידות שמוגדרות כ'מאגר' עלולות להיכשל בגלל בעיה בחלק מהקוד התלוי, כמו קוד מסד הנתונים.
Test Doubles
הפתרון הוא שבמהלך בדיקת המאגר, לא משתמשים בקוד האמיתי של הרשת או של מסד הנתונים, אלא משתמשים במקום זאת ב-test double. כפילת בדיקה היא גרסה של מחלקה שנוצרה במיוחד לצורך בדיקה. היא נועדה להחליף את הגרסה האמיתית של כיתה בבדיקות. זה דומה למצב שבו כפיל פעלולים הוא שחקן שמתמחה בפעלולים, ומחליף את השחקן האמיתי בפעולות מסוכנות.
אלה כמה סוגים של כפילים לבדיקה:
מזויף | כפיל בדיקה שיש לו הטמעה 'עובדת' של המחלקה, אבל ההטמעה נעשית באופן שמתאים לבדיקות אבל לא מתאים לייצור. |
Mock | כפיל בדיקה שעוקב אחרי המתודות שנקראו. לאחר מכן, הבדיקה עוברת או נכשלת בהתאם לשאלה אם בוצע קריאה נכונה לשיטות שלה. |
Stub | כפיל בדיקה שלא כולל לוגיקה ומחזיר רק את מה שתוכנתם להחזיר. אפשר לתכנת |
Dummy | כפיל בדיקה שמועבר אבל לא בשימוש, למשל אם צריך לספק אותו כפרמטר. אם היה לכם |
Spy | כפיל מבחן שעוקב גם אחרי מידע נוסף. לדוגמה, אם יצרתם |
מידע נוסף על כפילים לבדיקה זמין במאמר Testing on the Toilet: Know Your Test Doubles.
הכפילים הנפוצים ביותר לבדיקה ב-Android הם Fakes ו-Mocks.
במשימה הזו תיצרו FakeDataSource כפילת בדיקה כדי לבצע בדיקת יחידה של DefaultTasksRepository בניתוק ממקורות הנתונים בפועל.
שלב 1: יוצרים את המחלקה FakeDataSource
בשלב הזה תיצרו מחלקה בשם FakeDataSouce, שתהיה כפילה לבדיקה של LocalDataSource ושל RemoteDataSource.
- בסט המקורות test, לוחצים לחיצה ימנית ובוחרים באפשרות New -> Package (חדש -> חבילה).

- יוצרים חבילת נתונים עם חבילת מקור בתוכה.
- יוצרים מחלקה חדשה בשם
FakeDataSourceבחבילה data/source.

שלב 2: הטמעה של הממשק TasksDataSource
כדי שתוכלו להשתמש בכיתה החדשה FakeDataSource ככפיל לבדיקה, היא צריכה להיות מסוגלת להחליף את מקורות הנתונים האחרים. מקורות הנתונים האלה הם TasksLocalDataSource ו-TasksRemoteDataSource.

- שימו לב ששני הקודים האלה מטמיעים את הממשק
TasksDataSource.
class TasksLocalDataSource internal constructor(
private val tasksDao: TasksDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }
object TasksRemoteDataSource : TasksDataSource { ... }- גורמים ל-
FakeDataSourceלהטמיע אתTasksDataSource:
class FakeDataSource : TasksDataSource {
}מערכת Android Studio תתריע שלא הטמעתם את ה-methods הנדרשים עבור TasksDataSource.
- משתמשים בתפריט לתיקון מהיר ובוחרים באפשרות הטמעה של מועדון החברים.

- בוחרים את כל השיטות ולוחצים על אישור.

שלב 3: מטמיעים את השיטה getTasks ב-FakeDataSource
FakeDataSource הוא סוג ספציפי של כפילת בדיקה שנקרא זיוף. זיוף הוא כפיל בדיקה שיש לו הטמעה "עובדת" של המחלקה, אבל ההטמעה נעשית באופן שמתאים לבדיקות אבל לא מתאים לייצור. הטמעה 'פועלת' פירושה שהכיתה תפיק פלטים ריאליסטיים בהינתן קלטים.
לדוגמה, מקור הנתונים המזויף לא יתחבר לרשת ולא ישמור שום דבר במסד נתונים – במקום זאת, הוא ישתמש רק ברשימה בזיכרון. השיטה הזו תפעל כמו שאתם מצפים, כלומר שיטות להשגת משימות או לשמירתן יחזירו תוצאות צפויות, אבל לא תוכלו להשתמש בהטמעה הזו בסביבת ייצור, כי היא לא נשמרת בשרת או במסד נתונים.
FakeDataSource
- מאפשרת לכם לבדוק את הקוד ב-
DefaultTasksRepositoryבלי להסתמך על מסד נתונים או רשת אמיתיים. - מספק הטמעה 'מספיק אמיתית' לבדיקות.
- משנים את הבונה
FakeDataSourceכדי ליצורvarבשםtasksשהואMutableList<Task>?עם ערך ברירת מחדל של רשימה ריקה שניתנת לשינוי.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }
זו רשימת המשימות שמתחזות לתגובה של מסד נתונים או שרת. בינתיים, המטרה היא לבדוק את שיטת getTasks של המאגר . הפעולה הזו קוראת לשיטות getTasks, deleteAllTasks ו-saveTask של מקור הנתונים .
תכתוב גרסה מזויפת של השיטות האלה:
- Write
getTasks: Iftasksisn'tnull, return aSuccessresult. אםtasksהואnull, מחזירה תוצאה שלError. - כתיבה של
deleteAllTasks: ניקוי רשימת המשימות שניתנות לשינוי. - כותבים
saveTask: המשימה תתווסף לרשימה.
השיטות האלה, שמוטמעות עבור FakeDataSource, נראות כמו הקוד שבהמשך.
override suspend fun getTasks(): Result<List<Task>> {
tasks?.let { return Success(ArrayList(it)) }
return Error(
Exception("Tasks not found")
)
}
override suspend fun deleteAllTasks() {
tasks?.clear()
}
override suspend fun saveTask(task: Task) {
tasks?.add(task)
}הצהרות הייבוא שצריך להוסיף:
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Taskזה דומה לאופן שבו פועלים מקורות הנתונים המקומיים והמרוחקים בפועל.
בשלב הזה, תשתמשו בטכניקה שנקראת הזרקת תלות ידנית כדי שתוכלו להשתמש ב-test double המזויף שיצרתם.
הבעיה העיקרית היא שיש לך FakeDataSource, אבל לא ברור איך את משתמשת בו בבדיקות. היא צריכה להחליף את TasksRemoteDataSource ואת TasksLocalDataSource, אבל רק בבדיקות. המחלקות TasksRemoteDataSource ו-TasksLocalDataSource הן תלויות של DefaultTasksRepository, כלומר המחלקה DefaultTasksRepositories דורשת את המחלקות האלה או "תלויה" בהן כדי לפעול.
בשלב הזה, יחסי התלות נוצרים בתוך השיטה init של DefaultTasksRepository.
DefaultTasksRepository.kt
class DefaultTasksRepository private constructor(application: Application) {
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
// Some other code
init {
val database = Room.databaseBuilder(application.applicationContext,
ToDoDatabase::class.java, "Tasks.db")
.build()
tasksRemoteDataSource = TasksRemoteDataSource
tasksLocalDataSource = TasksLocalDataSource(database.taskDao())
}
// Rest of class
}מכיוון שאתם יוצרים ומקצים את taskLocalDataSource ו-tasksRemoteDataSource בתוך DefaultTasksRepository, הם למעשה מוצפנים. אין אפשרות להחליף את הכפיל של הבדיקה.
במקום זאת, כדאי לספק את מקורות הנתונים האלה למחלקה, במקום להגדיר אותם בהגדרות קשיחות. התהליך של אספקת יחסי תלות נקרא הזרקת תלות. יש דרכים שונות לספק תלויות, ולכן יש סוגים שונים של הזרקת תלויות.
הזרקת תלות של Constructor מאפשרת להחליף את הכפיל לבדיקה על ידי העברתו אל ה-constructor.
No injection
| החדרה
|
שלב 1: שימוש בהזרקת תלות של Constructor ב-DefaultTasksRepository
- משנים את ה-constructor של
DefaultTaskRepositoryכך שיקבל גם את מקורות הנתונים וגם את ה-dispatcher של ה-Coroutine (תצטרכו גם להחליף אותו בבדיקות – זה מתואר בפירוט רב יותר בקטע השלישי של השיעור בנושא Coroutines).Application
DefaultTasksRepository.kt
// REPLACE
class DefaultTasksRepository private constructor(application: Application) { // Rest of class }
// WITH
class DefaultTasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { // Rest of class }- מכיוון שהעברת את התלויות, צריך להסיר את השיטה
init. כבר לא צריך ליצור את התלות. - מוחקים גם את משתני המופע הישנים. אתם מגדירים אותם ב-constructor:
DefaultTasksRepository.kt
// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO- לבסוף, מעדכנים את השיטה
getRepositoryכדי להשתמש בבונה החדש:
DefaultTasksRepository.kt
companion object {
@Volatile
private var INSTANCE: DefaultTasksRepository? = null
fun getRepository(app: Application): DefaultTasksRepository {
return INSTANCE ?: synchronized(this) {
val database = Room.databaseBuilder(app,
ToDoDatabase::class.java, "Tasks.db")
.build()
DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
INSTANCE = it
}
}
}
}אתם משתמשים עכשיו בהזרקת תלות של בנאי!
שלב 2: שימוש ב-FakeDataSource בבדיקות
עכשיו, אחרי שהקוד משתמש בהזרקת תלות של בנאי, אפשר להשתמש במקור הנתונים המזויף כדי לבדוק את DefaultTasksRepository.
- לוחצים לחיצה ימנית על
DefaultTasksRepositoryשם המחלקה ובוחרים באפשרות יצירה ואז באפשרות בדיקה. - פועלים לפי ההוראות כדי ליצור
DefaultTasksRepositoryTestבערכת המקורות test. - בחלק העליון של המחלקה החדשה
DefaultTasksRepositoryTest, מוסיפים את משתני החברים שבהמשך כדי לייצג את הנתונים במקורות הנתונים המזויפים.
DefaultTasksRepositoryTest.kt
private val task1 = Task("Title1", "Description1")
private val task2 = Task("Title2", "Description2")
private val task3 = Task("Title3", "Description3")
private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
private val localTasks = listOf(task3).sortedBy { it.id }
private val newTasks = listOf(task3).sortedBy { it.id }- יוצרים שלושה משתנים, שני
FakeDataSourceמשתני חברים (אחד לכל מקור נתונים במאגר) ומשתנה ל-DefaultTasksRepositoryשייבדק.
DefaultTasksRepositoryTest.kt
private lateinit var tasksRemoteDataSource: FakeDataSource
private lateinit var tasksLocalDataSource: FakeDataSource
// Class under test
private lateinit var tasksRepository: DefaultTasksRepositoryיוצרים שיטה להגדרה ולאתחול של DefaultTasksRepository שאפשר לבדוק. ה-DefaultTasksRepository הזה ישתמש בכפילת הבדיקה שלך, FakeDataSource.
- יוצרים שיטה בשם
createRepositoryומוסיפים לה את ההערה@Before. - יוצרים מופעים של מקורות הנתונים המזויפים באמצעות הרשימות
remoteTasksו-localTasks. - מפעילים את
tasksRepositoryבאמצעות שני מקורות הנתונים המזויפים שיצרתם ו-Dispatchers.Unconfined.
השיטה הסופית צריכה להיראות כמו הקוד שבהמשך.
DefaultTasksRepositoryTest.kt
@Before
fun createRepository() {
tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
// Get a reference to the class under test
tasksRepository = DefaultTasksRepository(
// TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
// this requires understanding more about coroutines + testing
// so we will keep this as Unconfined for now.
tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
)
}שלב 3: כותבים את הבדיקה DefaultTasksRepository getTasks()
הגיע הזמן לכתוב מבחן DefaultTasksRepository!
- כותבים בדיקה לשיטה
getTasksבמאגר. בודקים שכאשר קוראים ל-getTasksעםtrue(כלומר, צריך לטעון מחדש ממקור הנתונים המרוחק) מוחזרים נתונים ממקור הנתונים המרוחק (ולא ממקור הנתונים המקומי).
DefaultTasksRepositoryTest.kt
@Test
fun getTasks_requestsAllTasksFromRemoteDataSource(){
// When tasks are requested from the tasks repository
val tasks = tasksRepository.getTasks(true) as Success
// Then tasks are loaded from the remote data source
assertThat(tasks.data, IsEqual(remoteTasks))
}תופיע שגיאה כשמתקשרים אל getTasks:
שלב 4: מוסיפים את הפונקציה runBlockingTest
השגיאה של הקורוטינה צפויה כי getTasks היא פונקציית suspend וצריך להפעיל קורוטינה כדי לקרוא לה. לשם כך, צריך להשתמש בהיקף של קורוטינה. כדי לפתור את השגיאה הזו, צריך להוסיף כמה תלויות של Gradle לטיפול בהפעלת קורוטינות בבדיקות.
- מוסיפים את התלויות הנדרשות לבדיקת קורוטינות למערך מקור הבדיקה באמצעות
testImplementation.
app/build.gradle
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"אל תשכחו לסנכרן!
kotlinx-coroutines-test היא ספריית הבדיקות של קורוטינות, שמיועדת במיוחד לבדיקת קורוטינות. כדי להריץ את הבדיקות, משתמשים בפונקציה runBlockingTest. זו פונקציה שמסופקת על ידי ספריית הבדיקה של קורוטינות. הוא מקבל בלוק קוד ואז מריץ את בלוק הקוד הזה בהקשר מיוחד של קורוטינה שפועלת באופן סינכרוני ומיידי, כלומר הפעולות יתרחשו בסדר דטרמיניסטי. הפעולה הזו גורמת לקורוטינות לפעול כמו פונקציות רגילות, ולכן היא מיועדת לבדיקת קוד.
משתמשים בפונקציה runBlockingTest בכיתות הבדיקה כשמפעילים פונקציה suspend. ב-codelab הבא בסדרה הזו נסביר איך runBlockingTest פועל ואיך בודקים קורוטינות.
- מוסיפים את
@ExperimentalCoroutinesApiמעל הכיתה. ההצהרה הזו מציינת שאתם יודעים שאתם משתמשים בממשק API ניסיוני של קורוטינה (runBlockingTest) במחלקה. אם לא תעשו זאת, תקבלו אזהרה. - חוזרים אל
DefaultTasksRepositoryTestומוסיפיםrunBlockingTestכדי שהבדיקה כולה תתקבל כ'בלוק' של קוד
הבדיקה הסופית נראית כמו הקוד שבהמשך.
DefaultTasksRepositoryTest.kt
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.core.IsEqual
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
@ExperimentalCoroutinesApi
class DefaultTasksRepositoryTest {
private val task1 = Task("Title1", "Description1")
private val task2 = Task("Title2", "Description2")
private val task3 = Task("Title3", "Description3")
private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
private val localTasks = listOf(task3).sortedBy { it.id }
private val newTasks = listOf(task3).sortedBy { it.id }
private lateinit var tasksRemoteDataSource: FakeDataSource
private lateinit var tasksLocalDataSource: FakeDataSource
// Class under test
private lateinit var tasksRepository: DefaultTasksRepository
@Before
fun createRepository() {
tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
// Get a reference to the class under test
tasksRepository = DefaultTasksRepository(
// TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
// this requires understanding more about coroutines + testing
// so we will keep this as Unconfined for now.
tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
)
}
@Test
fun getTasks_requestsAllTasksFromRemoteDataSource() = runBlockingTest {
// When tasks are requested from the tasks repository
val tasks = tasksRepository.getTasks(true) as Success
// Then tasks are loaded from the remote data source
assertThat(tasks.data, IsEqual(remoteTasks))
}
}- מריצים את הבדיקה החדשה
getTasks_requestsAllTasksFromRemoteDataSourceומוודאים שהיא פועלת והשגיאה נעלמה.
במאמר הזה ראיתם איך לבצע בדיקת יחידות במאגר. בשלבים הבאים, נשתמש שוב בהזרקת תלות וניצור עוד כפילת בדיקה – הפעם כדי להראות איך לכתוב בדיקות יחידה ובדיקות שילוב עבור מודלים של תצוגות.
בבדיקות יחידה צריך לבדוק רק את המחלקה או את השיטה שמעניינות אתכם. הבדיקה הזו נקראת בדיקה בבידוד, שבה מבודדים בבירור את ה "יחידה" ובודקים רק את הקוד ששייך ליחידה הזו.
לכן, TasksViewModelTest צריך לבדוק רק קוד TasksViewModel – הוא לא צריך לבדוק במסד הנתונים, ברשת או במחלקות המאגר. לכן, בשביל מודלים של תצוגות, בדומה למה שעשיתם עכשיו בשביל המאגר, תיצרו מאגר מזויף ותשתמשו בהזרקת תלות כדי להשתמש בו בבדיקות.
במשימה הזו תשתמשו בהזרקת תלות כדי להציג מודלים.

שלב 1. יצירת ממשק TasksRepository
השלב הראשון בשימוש בהזרקת תלות של בנאי הוא ליצור ממשק משותף בין המחלקה המזויפת למחלקה האמיתית.
איך זה נראה בפועל? אפשר לראות שTasksRemoteDataSource, TasksLocalDataSource ו-FakeDataSource חולקים את אותו ממשק: TasksDataSource. כך אפשר לציין בקונסטרוקטור של DefaultTasksRepository שאתם מקבלים TasksDataSource.
DefaultTasksRepository.kt
class DefaultTasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {כך אנחנו יכולים להחליף את FakeDataSource שלכם!
לאחר מכן, יוצרים ממשק ל-DefaultTasksRepository, כמו שעשיתם למקורות הנתונים. היא צריכה לכלול את כל השיטות הציבוריות (public API surface) של DefaultTasksRepository.
- פותחים את
DefaultTasksRepositoryולוחצים לחיצה ימנית על שם הכיתה. לאחר מכן בוחרים באפשרות Refactor -> Extract -> Interface (שינוי מבנה -> חילוץ -> ממשק).

- בוחרים באפשרות חילוץ לקובץ נפרד.

- בחלון Extract Interface, משנים את שם הממשק ל-
TasksRepository. - בקטע Members to form interface, מסמנים את כל החברים חוץ משני החברים במצב Companion והשיטות private.

- לוחצים על Refactor (שיפור קוד). ממשק
TasksRepositoryהחדש אמור להופיע בחבילה data/source .

ועכשיו DefaultTasksRepository מטמיע את TasksRepository.
- מריצים את האפליקציה (לא את הבדיקות) כדי לוודא שהכול עדיין תקין.
שלב 2. יצירת FakeTestRepository
עכשיו, אחרי שיש לכם את הממשק, אתם יכולים ליצור את DefaultTaskRepository כפילת הבדיקה.
- במערך מקורות הבדיקה, בתיקייה data/source, יוצרים את קובץ ה-Kotlin ואת המחלקה
FakeTestRepository.ktומרחיבים מהממשקTasksRepository.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
}תקבלו הודעה שאתם צריכים להטמיע את שיטות הממשק.
- מעבירים את העכבר מעל השגיאה עד שמופיע תפריט ההצעות, ואז לוחצים על Implement members (הטמעה של חברים).
- בוחרים את כל השיטות ולוחצים על אישור.

שלב 3. הטמעה של שיטות FakeTestRepository
עכשיו יש לך מחלקה FakeTestRepository עם שיטות שלא יושמו. בדומה לאופן שבו הטמעתם את FakeDataSource, ה-FakeTestRepository יגובה על ידי מבנה נתונים, במקום להתמודד עם תיווך מסובך בין מקורות נתונים מקומיים ומרוחקים.
שימו לב: FakeTestRepository לא צריך להשתמש ב-FakeDataSource או במשהו דומה. הוא רק צריך להחזיר פלט מזויף ריאליסטי בהינתן קלט. תשתמשו ב-LinkedHashMap כדי לאחסן את רשימת המשימות וב-MutableLiveData כדי לעקוב אחרי המשימות.
- ב-
FakeTestRepository, מוסיפים גם משתנהLinkedHashMapשמייצג את רשימת המשימות הנוכחית וגםMutableLiveDataלמשימות שניתן לצפות בהן.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private val observableTasks = MutableLiveData<Result<List<Task>>>()
// Rest of class
}מטמיעים את השיטות הבאות:
-
getTasks—השיטה הזו צריכה לקחת אתtasksServiceDataולהפוך אותו לרשימה באמצעותtasksServiceData.values.toList()ואז להחזיר את הרשימה כתוצאהSuccess. -
refreshTasks– מעדכן את הערך שלobservableTasksלערך שמוחזר על ידיgetTasks(). -
observeTasks– יוצר קורוטינה באמצעותrunBlockingומריץ אתrefreshTasks, ואז מחזיר אתobservableTasks.
בהמשך מופיע הקוד של השיטות האלה.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private val observableTasks = MutableLiveData<Result<List<Task>>>()
override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
return Result.Success(tasksServiceData.values.toList())
}
override suspend fun refreshTasks() {
observableTasks.value = getTasks()
}
override fun observeTasks(): LiveData<Result<List<Task>>> {
runBlocking { refreshTasks() }
return observableTasks
}
// Rest of class
}שלב 4. הוספת שיטה לבדיקה אל addTasks
כשבודקים, עדיף שיהיו כבר כמה Tasks במאגר. אפשר לקרוא ל-saveTask כמה פעמים, אבל כדי לפשט את התהליך, כדאי להוסיף שיטת עזר במיוחד לבדיקות, שתאפשר לכם להוסיף משימות.
- מוסיפים את השיטה
addTasks, שמקבלתvarargשל משימות, מוסיפה כל אחת מהן ל-HashMapואז מרעננת את המשימות.
FakeTestRepository.kt
fun addTasks(vararg tasks: Task) {
for (task in tasks) {
tasksServiceData[task.id] = task
}
runBlocking { refreshTasks() }
}בשלב הזה יש לכם מאגר מזויף לבדיקה עם כמה מהשיטות העיקריות שהוטמעו. אחר כך, משתמשים בזה בבדיקות.
במשימה הזו תשתמשו במחלקה מזויפת בתוך ViewModel. משתמשים בהזרקת תלות של בנאי כדי לקבל את שני מקורות הנתונים באמצעות הזרקת תלות של בנאי. לשם כך, מוסיפים משתנה TasksRepository לבנאי של TasksViewModel.
התהליך הזה קצת שונה כשמדובר במודלים של תצוגות, כי לא בונים אותם ישירות. לדוגמה:
class TasksFragment : Fragment() {
private val viewModel by viewModels<TasksViewModel>()
// Rest of class...
}
כמו בקוד שלמעלה, אתם משתמשים בviewModel's property delegate שיוצר את מודל התצוגה. כדי לשנות את אופן הבנייה של מודל התצוגה, צריך להוסיף ולהשתמש ב-ViewModelProvider.Factory. אם אתם לא מכירים את ViewModelProvider.Factory, תוכלו לקרוא מידע נוסף על כאן.
שלב 1. יצירה של ViewModelFactory ושימוש בו ב-TasksViewModel
מתחילים בעדכון הכיתות והמבחן שקשורים למסך Tasks.
- פותחים את
TasksViewModel. - משנים את ה-constructor של
TasksViewModelכך שיקבל אתTasksRepositoryבמקום ליצור אותו בתוך המחלקה.
TasksViewModel.kt
// REPLACE
class TasksViewModel(application: Application) : AndroidViewModel(application) {
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// Rest of class
}
// WITH
class TasksViewModel( private val tasksRepository: TasksRepository ) : ViewModel() {
// Rest of class
}שינית את ה-constructor, ולכן עכשיו צריך להשתמש ב-factory כדי ליצור את TasksViewModel. ממקמים את מחלקת היצרן באותו קובץ כמו TasksViewModel, אבל אפשר גם למקם אותה בקובץ משלה.
- בתחתית הקובץ
TasksViewModel, מחוץ לכיתה, מוסיפיםTasksViewModelFactoryשמקבלTasksRepositoryרגיל.
TasksViewModel.kt
@Suppress("UNCHECKED_CAST")
class TasksViewModelFactory (
private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>) =
(TasksViewModel(tasksRepository) as T)
}
זו הדרך הרגילה לשנות את האופן שבו נוצרים ViewModel. עכשיו, אחרי שיש לכם את ה-factory, אתם יכולים להשתמש בו בכל מקום שבו אתם בונים את מודל התצוגה.
- צריך לעדכן את
TasksFragmentכדי להשתמש במפעל.
TasksFragment.kt
// REPLACE
private val viewModel by viewModels<TasksViewModel>()
// WITH
private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}- מריצים את הקוד של האפליקציה ומוודאים שהכול עדיין עובד.
שלב 2. שימוש ב-FakeTestRepository בתוך TasksViewModelTest
עכשיו, במקום להשתמש במאגר האמיתי בבדיקות של מודל התצוגה, אפשר להשתמש במאגר המזויף.
- פותחים את
TasksViewModelTest. - מוסיפים נכס
FakeTestRepositoryב-TasksViewModelTest.
TaskViewModelTest.kt
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
// Use a fake repository to be injected into the viewmodel
private lateinit var tasksRepository: FakeTestRepository
// Rest of class
}- מעדכנים את השיטה
setupViewModelכדי ליצורFakeTestRepositoryעם שלוש משימות, ואז בונים אתtasksViewModelעם המאגר הזה.
TasksViewModelTest.kt
@Before
fun setupViewModel() {
// We initialise the tasks to 3, with one active and two completed
tasksRepository = FakeTestRepository()
val task1 = Task("Title1", "Description1")
val task2 = Task("Title2", "Description2", true)
val task3 = Task("Title3", "Description3", true)
tasksRepository.addTasks(task1, task2, task3)
tasksViewModel = TasksViewModel(tasksRepository)
}- מכיוון שאתם כבר לא משתמשים בקוד AndroidX Test
ApplicationProvider.getApplicationContext, אתם יכולים גם להסיר את ההערה@RunWith(AndroidJUnit4::class). - מריצים את הבדיקות ומוודאים שכולן עדיין פועלות.
באמצעות הזרקת תלות של בנאי, הסרתם את DefaultTasksRepository כתלות והחלפתם אותה ב-FakeTestRepository בבדיקות.
שלב 3. עדכון גם של TaskDetail Fragment ו-ViewModel
מבצעים את אותם שינויים בדיוק במאפיינים TaskDetailFragment ו-TaskDetailViewModel. כך הקוד יהיה מוכן כשכותבים TaskDetail tests בפעם הבאה.
- פותחים את
TaskDetailViewModel. - מעדכנים את ה-constructor:
TaskDetailViewModel.kt
// REPLACE
class TaskDetailViewModel(application: Application) : AndroidViewModel(application) {
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// Rest of class
}
// WITH
class TaskDetailViewModel(
private val tasksRepository: TasksRepository
) : ViewModel() { // Rest of class }- בתחתית הקובץ
TaskDetailViewModel, מחוץ לכיתה, מוסיפיםTaskDetailViewModelFactory.
TaskDetailViewModel.kt
@Suppress("UNCHECKED_CAST")
class TaskDetailViewModelFactory (
private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>) =
(TaskDetailViewModel(tasksRepository) as T)
}- צריך לעדכן את
TasksFragmentכדי להשתמש במפעל.
TasksFragment.kt
// REPLACE
private val viewModel by viewModels<TaskDetailViewModel>()
// WITH
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}- מריצים את הקוד ומוודאים שהכול עובד.
עכשיו אפשר להשתמש ב-FakeTestRepository במקום במאגר האמיתי ב-TasksFragment וב-TasksDetailFragment.
בשלב הבא תכתבו בדיקות שילוב כדי לבדוק את האינטראקציות בין ה-Fragment לבין ה-ViewModel. תוכלו לדעת אם קוד מודל התצוגה מעדכן את ממשק המשתמש בצורה מתאימה. כדי לעשות את זה, משתמשים
- הדפוס ServiceLocator
- הספריות Espresso ו-Mockito
בדיקות שילוב בודקות את האינטראקציה בין כמה מחלקות כדי לוודא שהן מתנהגות כמצופה כשמשתמשים בהן יחד. אפשר להריץ את הבדיקות האלה באופן מקומי (test source set) או כבדיקות מכשור (androidTest source set).

במקרה שלכם, תיקחו כל קטע ותכתבו בדיקות שילוב לקטע ולמודל התצוגה כדי לבדוק את התכונות העיקריות של הקטע.
שלב 1. הוספת יחסי תלות ב-Gradle
- מוסיפים את יחסי התלות הבאים של Gradle.
app/build.gradle
// Dependencies for Android instrumented unit tests
androidTestImplementation "junit:junit:$junitVersion"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
// Testing code should not be included in the main code.
// Once https://issuetracker.google.com/128612536 is fixed this can be fixed.
implementation "androidx.fragment:fragment-testing:$fragmentVersion"
implementation "androidx.test:core:$androidXTestCoreVersion"
יחסי התלות האלה כוללים:
-
junit:junit—JUnit, שנדרש לכתיבת הצהרות בדיקה בסיסיות. -
androidx.test:core—Core AndroidX test library -
kotlinx-coroutines-test– ספריית הבדיקות של שגרות המשנה -
androidx.fragment:fragment-testing—ספריית הבדיקה AndroidX ליצירת רכיבי Fragment בבדיקות ולשינוי המצב שלהם.
מכיוון שתשתמשו בספריות האלה בערכת המקורות androidTest, צריך להשתמש ב-androidTestImplementation כדי להוסיף אותן כתלויות.
שלב 2. יצירת מחלקה TaskDetailFragmentTest
ב-TaskDetailFragment מוצג מידע על משימה אחת.

תתחילו בכתיבת בדיקת קטע בשביל TaskDetailFragment, כי הפונקציונליות שלו די בסיסית בהשוואה לקטעים אחרים.
- פותחים את
taskdetail.TaskDetailFragment. - תצור בדיקה ל-
TaskDetailFragment, כמו שעשית קודם. מאשרים את ברירות המחדל ומציבים אותו בערכת המקור androidTest (ולא בערכת המקורtest).

- מוסיפים את ההערות הבאות לכיתה
TaskDetailFragmentTest.
TaskDetailFragmentTest.kt
@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
}מטרת ההערות האלה היא:
-
@MediumTest– מציין שהבדיקה היא בדיקת אינטגרציה עם זמן ריצה בינוני (לעומת בדיקות יחידה@SmallTestובדיקות מקצה לקצה@LargeTest). כך תוכלו לקבץ את הבדיקות ולבחור את הגודל שלהן. -
@RunWith(AndroidJUnit4::class)– משמש בכל כיתה שמשתמשת ב-AndroidX Test.
שלב 3. הפעלת קטע מתוך בדיקה
במשימה הזו, תפעילו את TaskDetailFragment באמצעות ספריית הבדיקות של AndroidX. FragmentScenario היא מחלקה מ-AndroidX Test שעוטפת קטע ומאפשרת לכם שליטה ישירה במחזור החיים של הקטע לצורך בדיקה. כדי לכתוב בדיקות עבור קטעים, יוצרים FragmentScenario עבור הקטע שרוצים לבדוק (TaskDetailFragment).
- מעתיקים את הבדיקה הזו אל
TaskDetailFragmentTest.
TaskDetailFragmentTest.kt
@Test
fun activeTaskDetails_DisplayedInUi() {
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
הקוד שלמעלה:
- יוצר משימה.
- יוצרת
Bundle, שמייצגת את הארגומנטים של הקטע במשימה שמועברים לקטע). - הפונקציה
launchFragmentInContainerיוצרתFragmentScenario, עם החבילה הזו ועם עיצוב.
זו לא בדיקה שהסתיימה, כי היא לא מאשרת שום דבר. בינתיים, מריצים את הבדיקה ומתבוננים במה שקורה.
- זו בדיקה עם מכשור, לכן חשוב לוודא שהאמולטור או המכשיר גלויים.
- מריצים את הבדיקה.
צריכים לקרות כמה דברים.
- קודם כל, מכיוון שמדובר בבדיקה עם מכשור, הבדיקה תפעל במכשיר הפיזי (אם הוא מחובר) או באמולטור.
- הפרגמנט אמור להיפתח.
- שימו לב שלא עוברים דרך אף פרגמנט אחר ואין תפריטים שמשויכים לפעילות – זה רק הפרגמנט.
לבסוף, בוחנים את הקטע מקרוב ורואים שכתוב בו 'אין נתונים', כי נתוני המשימה לא נטענו בהצלחה.

הבדיקה צריכה לטעון את TaskDetailFragment (כפי שעשיתם) ולוודא שהנתונים נטענו בצורה נכונה. למה אין נתונים? הסיבה לכך היא שיצרתם משימה, אבל לא שמרתם אותה במאגר.
@Test
fun activeTaskDetails_DisplayedInUi() {
// This DOES NOT save the task anywhere
val activeTask = Task("Active Task", "AndroidX Rocks", false)
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
יש לך את FakeTestRepository, אבל אתה צריך דרך להחליף את המאגר האמיתי במאגר המזויף בשביל הקטע. זה מה שתעשו עכשיו.
במשימה הזו תספקו את מאגר הדמה שלכם לשבר באמצעות ServiceLocator. כך תוכלו לכתוב את הקטע ולראות את בדיקות השילוב של המודל.
אי אפשר להשתמש כאן בהזרקת תלות של בנאי, כמו שעשיתם קודם, כשנדרש לספק תלות במודל התצוגה או במאגר. הזרקת תלות של בנאי מחייבת בנייה של המחלקה. Fragments ו-activities הן דוגמאות למחלקות שאתם לא בונים ובדרך כלל אין לכם גישה לבונה שלהן.
מכיוון שלא בונים את הפרגמנט, אי אפשר להשתמש בהזרקת תלות של בנאי כדי להחליף את כפיל הבדיקה של המאגר (FakeTestRepository) בפרגמנט. במקום זאת, כדאי להשתמש בתבנית Service Locator. התבנית Service Locator היא חלופה ל-Dependency Injection. היא כוללת יצירה של מחלקה יחידה (singleton) שנקראת Service Locator, שמטרתה לספק תלות, גם לקוד הרגיל וגם לקוד הבדיקה. בקוד האפליקציה הרגיל (קבוצת המקור main), כל התלויות האלה הן תלויות רגילות של האפליקציה. לצורך הבדיקות, משנים את Service Locator כדי לספק גרסאות כפולות של התלויות.
לא משתמשים בכלי לאיתור שירותים
| שימוש בכלי לאיתור שירותים
|
באפליקציית ה-Codelab הזו, מבצעים את הפעולות הבאות:
- יוצרים מחלקה של Service Locator שיכולה ליצור ולאחסן מאגר. כברירת מחדל, הוא יוצר מאגר 'רגיל'.
- משנים את המבנה של הקוד כך שכשתזדקקו למאגר, תשתמשו ב-Service Locator.
- במחלקת הבדיקה, קוראים לשיטה ב-Service Locator שמחליפה את המאגר 'הרגיל' ב-test double.
שלב 1. יצירת ServiceLocator
בואו ניצור כיתה בשם ServiceLocator. הוא יישמר בקבוצת המקור הראשית עם שאר קוד האפליקציה, כי קוד האפליקציה הראשי משתמש בו.
הערה: ServiceLocator הוא סינגלטון, ולכן צריך להשתמש במילת המפתח Kotlin object עבור המחלקה.
- יוצרים את הקובץ ServiceLocator.kt ברמה העליונה של קבוצת המקורות הראשית.
- הגדרת
objectבשםobject.ServiceLocator - יוצרים משתני מופע
databaseו-repositoryומגדירים את שניהם ל-null. - מוסיפים את ההערה
@Volatileלמאגר כי יכול להיות שייעשה בו שימוש בכמה שרשורים (ההסבר המפורט על@Volatileמופיע כאן).
הקוד שלכם צריך להיראות כמו הקוד שמוצג למטה.
object ServiceLocator {
private var database: ToDoDatabase? = null
@Volatile
var tasksRepository: TasksRepository? = null
}בשלב הזה, ServiceLocator צריך רק לדעת איך להחזיר TasksRepository. הפונקציה תחזיר DefaultTasksRepository קיים או תיצור DefaultTasksRepository חדש ותחזיר אותו, אם צריך.
מגדירים את הפונקציות הבאות:
-
provideTasksRepository– מספק מאגר קיים או יוצר מאגר חדש. צריך להשתמש בשיטה הזוsynchronizedב-thisכדי למנוע יצירה של שני מופעים של מאגר בטעות במצבים שבהם פועלים כמה שרשורים. -
createTasksRepository– קוד ליצירת מאגר חדש. תתבצע שיחה אלcreateTaskLocalDataSourceוייווצרTasksRemoteDataSourceחדש. -
createTaskLocalDataSource– קוד ליצירת מקור נתונים מקומי חדש. תתבצע התקשרות אלcreateDataBase. -
createDataBase– קוד ליצירת מסד נתונים חדש.
הקוד המלא מופיע בהמשך.
ServiceLocator.kt
object ServiceLocator {
private var database: ToDoDatabase? = null
@Volatile
var tasksRepository: TasksRepository? = null
fun provideTasksRepository(context: Context): TasksRepository {
synchronized(this) {
return tasksRepository ?: createTasksRepository(context)
}
}
private fun createTasksRepository(context: Context): TasksRepository {
val newRepo = DefaultTasksRepository(TasksRemoteDataSource, createTaskLocalDataSource(context))
tasksRepository = newRepo
return newRepo
}
private fun createTaskLocalDataSource(context: Context): TasksDataSource {
val database = database ?: createDataBase(context)
return TasksLocalDataSource(database.taskDao())
}
private fun createDataBase(context: Context): ToDoDatabase {
val result = Room.databaseBuilder(
context.applicationContext,
ToDoDatabase::class.java, "Tasks.db"
).build()
database = result
return result
}
}שלב 2. שימוש ב-ServiceLocator באפליקציה
אתם עומדים לבצע שינוי בקוד של האפליקציה הראשית (לא בבדיקות) כדי ליצור את המאגר במקום אחד, ServiceLocator.
חשוב ליצור רק מופע אחד של מחלקת המאגר. כדי לוודא זאת, תשתמשו ב-Service locator במחלקה Application.
- ברמה העליונה של היררכיית החבילות, פותחים את
TodoApplicationויוצריםvalלמאגר ומקצים לו מאגר שמתקבל באמצעותServiceLocator.provideTaskRepository.
TodoApplication.kt
class TodoApplication : Application() {
val taskRepository: TasksRepository
get() = ServiceLocator.provideTasksRepository(this)
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) Timber.plant(DebugTree())
}
}
אחרי שיוצרים מאגר באפליקציה, אפשר להסיר את השיטה הישנה getRepository ב-DefaultTasksRepository.
- פותחים את
DefaultTasksRepositoryומוחקים את האובייקט הנלווה.
DefaultTasksRepository.kt
// DELETE THIS COMPANION OBJECT
companion object {
@Volatile
private var INSTANCE: DefaultTasksRepository? = null
fun getRepository(app: Application): DefaultTasksRepository {
return INSTANCE ?: synchronized(this) {
val database = Room.databaseBuilder(app,
ToDoDatabase::class.java, "Tasks.db")
.build()
DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
INSTANCE = it
}
}
}
}מעכשיו, בכל מקום שבו השתמשתם ב-getRepository, תצטרכו להשתמש ב-taskRepository של האפליקציה. כך תוכלו לוודא שבמקום ליצור את המאגר ישירות, תקבלו את המאגר שסופק על ידי ServiceLocator.
- פותחים את
TaskDetailFragementומחפשים את השיחה עםgetRepositoryבחלק העליון של הכיתה. - מחליפים את הקריאה הזו בקריאה שמקבלת את המאגר מ-
TodoApplication.
TaskDetailFragment.kt
// REPLACE this code
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
// WITH this code
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
}- חוזרים על הפעולה גם עבור
TasksFragment.
TasksFragment.kt
// REPLACE this code
private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
// WITH this code
private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
}- במקרים של
StatisticsViewModelו-AddEditTaskViewModel, מעדכנים את הקוד שמקבל את המאגר כדי להשתמש במאגר מ-TodoApplication.
TasksFragment.kt
// REPLACE this code
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// WITH this code
private val tasksRepository = (application as TodoApplication).taskRepository
- מריצים את האפליקציה (לא את הבדיקה).
מכיוון שביצעתם רק רפקטורינג, האפליקציה אמורה לפעול באותו אופן ללא בעיות.
שלב 3. Create FakeAndroidTestRepository
כבר יש לך FakeTestRepository במערך מקורות הבדיקה. כברירת מחדל, אי אפשר לשתף מחלקות בדיקה בין קבוצות המקורות test ו-androidTest. לכן, צריך ליצור עותק של המחלקה FakeTestRepository במערך המקור androidTest ולקרוא לו FakeAndroidTestRepository.
- לוחצים לחיצה ימנית על קבוצת המקור
androidTestויוצרים חבילת נתונים. לוחצים שוב לחיצה ימנית ויוצרים חבילת מקור . - יוצרים מחלקה חדשה בחבילת המקור הזו בשם
FakeAndroidTestRepository.kt. - מעתיקים את הקוד הבא לכיתה הזו.
FakeAndroidTestRepository.kt
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.runBlocking
import java.util.LinkedHashMap
class FakeAndroidTestRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private var shouldReturnError = false
private val observableTasks = MutableLiveData<Result<List<Task>>>()
fun setReturnError(value: Boolean) {
shouldReturnError = value
}
override suspend fun refreshTasks() {
observableTasks.value = getTasks()
}
override suspend fun refreshTask(taskId: String) {
refreshTasks()
}
override fun observeTasks(): LiveData<Result<List<Task>>> {
runBlocking { refreshTasks() }
return observableTasks
}
override fun observeTask(taskId: String): LiveData<Result<Task>> {
runBlocking { refreshTasks() }
return observableTasks.map { tasks ->
when (tasks) {
is Result.Loading -> Result.Loading
is Error -> Error(tasks.exception)
is Success -> {
val task = tasks.data.firstOrNull() { it.id == taskId }
?: return@map Error(Exception("Not found"))
Success(task)
}
}
}
}
override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result<Task> {
if (shouldReturnError) {
return Error(Exception("Test exception"))
}
tasksServiceData[taskId]?.let {
return Success(it)
}
return Error(Exception("Could not find task"))
}
override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
if (shouldReturnError) {
return Error(Exception("Test exception"))
}
return Success(tasksServiceData.values.toList())
}
override suspend fun saveTask(task: Task) {
tasksServiceData[task.id] = task
}
override suspend fun completeTask(task: Task) {
val completedTask = Task(task.title, task.description, true, task.id)
tasksServiceData[task.id] = completedTask
}
override suspend fun completeTask(taskId: String) {
// Not required for the remote data source.
throw NotImplementedError()
}
override suspend fun activateTask(task: Task) {
val activeTask = Task(task.title, task.description, false, task.id)
tasksServiceData[task.id] = activeTask
}
override suspend fun activateTask(taskId: String) {
throw NotImplementedError()
}
override suspend fun clearCompletedTasks() {
tasksServiceData = tasksServiceData.filterValues {
!it.isCompleted
} as LinkedHashMap<String, Task>
}
override suspend fun deleteTask(taskId: String) {
tasksServiceData.remove(taskId)
refreshTasks()
}
override suspend fun deleteAllTasks() {
tasksServiceData.clear()
refreshTasks()
}
fun addTasks(vararg tasks: Task) {
for (task in tasks) {
tasksServiceData[task.id] = task
}
runBlocking { refreshTasks() }
}
}
שלב 4. הכנת ServiceLocator לבדיקות
אוקיי, הגיע הזמן להשתמש ב-ServiceLocator כדי להחליף את הכפילים בבדיקה. כדי לעשות את זה, צריך להוסיף קוד לקוד ServiceLocator.
- פותחים את
ServiceLocator.kt. - מסמנים את הפונקציה להגדרת הערך של
tasksRepositoryכ-@VisibleForTesting. ההערה הזו היא דרך לציין שהסיבה לכך שפונקציית ה-setter היא ציבורית היא בגלל בדיקות.
ServiceLocator.kt
@Volatile
var tasksRepository: TasksRepository? = null
@VisibleForTesting setהבדיקות צריכות לפעול בדיוק באותו אופן, בין אם מריצים אותן לבד או בקבוצת בדיקות. המשמעות היא שהבדיקות לא צריכות להסתמך על התנהגות שקשורה אחת לשנייה (כלומר, לא לשתף אובייקטים בין בדיקות).
מכיוון ש-ServiceLocator הוא singleton, יכול להיות שהוא ישותף בטעות בין בדיקות. כדי להימנע מכך, צריך ליצור שיטה שמאפסת את המצב של ServiceLocator בצורה נכונה בין הבדיקות.
- מוסיפים משתנה מופע בשם
lockעם הערךAny.
ServiceLocator.kt
private val lock = Any()- מוסיפים שיטה ספציפית לבדיקה בשם
resetRepositoryשמנקה את מסד הנתונים ומגדירה את המאגר ואת מסד הנתונים כ-null.
ServiceLocator.kt
@VisibleForTesting
fun resetRepository() {
synchronized(lock) {
runBlocking {
TasksRemoteDataSource.deleteAllTasks()
}
// Clear all data to avoid test pollution.
database?.apply {
clearAllTables()
close()
}
database = null
tasksRepository = null
}
}שלב 5. שימוש ב-ServiceLocator
בשלב הזה משתמשים ב-ServiceLocator.
- פותחים את
TaskDetailFragmentTest. - מצהירים על משתנה
lateinit TasksRepository. - מוסיפים שיטת הגדרה ושיטת הסרה כדי להגדיר
FakeAndroidTestRepositoryלפני כל בדיקה ולנקות אותו אחרי כל בדיקה.
TaskDetailFragmentTest.kt
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
- עוטפים את גוף הפונקציה של
activeTaskDetails_DisplayedInUi()ב-runBlockingTest. - שומרים את
activeTaskבמאגר לפני שמפעילים את הפריט.
repository.saveTask(activeTask)הבדיקה הסופית נראית כמו הקוד שבהמשך.
TaskDetailFragmentTest.kt
@Test
fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
repository.saveTask(activeTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}- להוסיף הערות לכל הכיתה באמצעות
@ExperimentalCoroutinesApi.
בסיום, הקוד ייראה כך.
TaskDetailFragmentTest.kt
@MediumTest
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
@Test
fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
repository.saveTask(activeTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
}
- מריצים את הבדיקה
activeTaskDetails_DisplayedInUi().
בדומה למקודם, אמור להופיע הקטע, אבל הפעם, בגלל שהגדרתם את המאגר בצורה נכונה, מוצג בו מידע על המשימה.

בשלב הזה, תשתמשו בספריית בדיקות ממשק המשתמש של Espresso כדי להשלים את בדיקת השילוב הראשונה. הקוד שלכם בנוי בצורה שמאפשרת להוסיף בדיקות עם הצהרות לממשק המשתמש. כדי לעשות זאת, תשתמשו בספריית הבדיקות של Espresso.
בעזרת Espresso תוכלו:
- אינטראקציות עם תצוגות, כמו לחיצה על לחצנים, הזזת סרגל או גלילה למטה במסך.
- הצהרה על כך שתצוגות מסוימות מופיעות במסך או שהן במצב מסוים (למשל, שהן מכילות טקסט מסוים, או שתיבת סימון מסוימת מסומנת וכו').
שלב 1. הערה לגבי תלות ב-Gradle
תלות ה-Espresso הראשית כבר תהיה קיימת, כי היא כלולה בפרויקטים של Android כברירת מחדל.
app/build.gradle
dependencies {
// ALREADY in your code
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
// Other dependencies
}androidx.test.espresso:espresso-core – התלות הזו ב-Espresso כלולה כברירת מחדל כשיוצרים פרויקט Android חדש. הוא מכיל את קוד הבדיקה הבסיסי לרוב התצוגות והפעולות בהן.
שלב 2. השבתת אנימציות
בדיקות Espresso מופעלות במכשיר אמיתי, ולכן הן בדיקות מכשור מטבען. בעיה אחת שעלולה לקרות היא אנימציות: אם אנימציה נתקעת ואתם מנסים לבדוק אם תצוגה מסוימת מופיעה במסך, אבל האנימציה עדיין פועלת, יכול להיות ש-Espresso יכשל בבדיקה בטעות. הדבר עלול לגרום לחוסר עקביות בבדיקות Espresso.
כדי לבצע בדיקות של ממשק המשתמש באמצעות Espresso, מומלץ להשבית את האנימציות (כך גם הבדיקה תפעל מהר יותר):
- במכשיר הבדיקה, עוברים אל הגדרות > אפשרויות למפתחים.
- משביתים את שלוש ההגדרות האלה: קנה מידה להנפשה של חלון, קנה מידה לאנימציית מעבר וקנה מידה למשך זמן אנימציה.

שלב 3. בדיקת Espresso
לפני שכותבים בדיקת Espresso, כדאי לעיין בקוד Espresso.
onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))ההצהרה הזו מוצאת את התצוגה של תיבת הסימון עם המזהה task_detail_complete_checkbox, לוחצת עליה ואז בודקת שהיא מסומנת.
רוב ההצהרות ב-Espresso מורכבות מארבעה חלקים:
onViewonView היא דוגמה לשיטה סטטית של Espresso שמתחילה הצהרת Espresso. onView היא אחת מהאפשרויות הנפוצות ביותר, אבל יש אפשרויות אחרות, כמו onData.
2. ViewMatcher
withId(R.id.task_detail_title_text)withId היא דוגמה לViewMatcher שמקבלת תצוגה לפי המזהה שלה. יש עוד התאמות להצגת תצוגה שאפשר למצוא בתיעוד.
3. ViewAction
perform(click())השיטה perform שמקבלת ViewAction. ViewAction היא פעולה שאפשר לבצע בתצוגה, למשל לחיצה על התצוגה.
check(matches(isChecked()))check שמשתמש בViewAssertion. ViewAssertions בודק או קובע משהו לגבי התצוגה. הטענה הנפוצה ביותר לשימוש היא matches.ViewAssertion כדי לסיים את ההצהרה, משתמשים בעוד ViewMatcher, במקרה הזה isChecked.

הערה: לא תמיד צריך לקרוא גם ל-perform וגם ל-check בהצהרת Espresso. אפשר להשתמש בהצהרות שכוללות רק טענה באמצעות check או רק פעולה באמצעות ViewAction עם perform.
- פותחים את
TaskDetailFragmentTest.kt. - מעדכנים את הבדיקה
activeTaskDetails_DisplayedInUi.
TaskDetailFragmentTest.kt
@Test
fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
repository.saveTask(activeTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_title_text)).check(matches(withText("Active Task")))
onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
// and make sure the "active" checkbox is shown unchecked
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(not(isChecked())))
}
הצהרות הייבוא, אם צריך:
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.core.IsNot.not- כל מה שמופיע אחרי התגובה
// THENמשתמש ב-Espresso. בודקים את מבנה הבדיקה ואת השימוש בתגwithIdכדי להצהיר איך דף הפרטים אמור להיראות. - מריצים את הבדיקה ומוודאים שהיא עוברת.
שלב 4. אופציונלי, כותבים בדיקת Espresso משלכם
עכשיו כותבים בעצמכם מבחן.
- יוצרים בדיקה חדשה בשם
completedTaskDetails_DisplayedInUiומעתיקים את קוד השלד הזה.
TaskDetailFragmentTest.kt
@Test
fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add completed task to the DB
// WHEN - Details fragment launched to display task
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
}- בהתבסס על הבדיקה הקודמת, השלם את הבדיקה הזו.
- מריצים את הבדיקה ומוודאים שהיא עוברת.
התג completedTaskDetails_DisplayedInUi הסופי צריך להיראות כמו הקוד הזה.
TaskDetailFragmentTest.kt
@Test
fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add completed task to the DB
val completedTask = Task("Completed Task", "AndroidX Rocks", true)
repository.saveTask(completedTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(completedTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_title_text)).check(matches(withText("Completed Task")))
onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
// and make sure the "active" checkbox is shown unchecked
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isChecked()))
}בשלב האחרון נסביר איך לבדוק את רכיב הניווט באמצעות סוג אחר של כפילת בדיקה שנקראת mock, וספריית הבדיקות Mockito.
ב-codelab הזה השתמשתם ב-test double שנקרא fake. Fakes הם אחד מתוך סוגים רבים של כפילים לבדיקה. באיזה כפיל בדיקה כדאי להשתמש כדי לבדוק את רכיב הניווט?
כדאי לחשוב איך הניווט מתבצע. תארו לעצמכם שאתם לוחצים על אחת המשימות בTasksFragment כדי לנווט למסך פרטי המשימה.

הנה קוד ב-TasksFragment שמנווט למסך פרטי המשימה כשלוחצים עליו.
TasksFragment.kt
private fun openTaskDetails(taskId: String) {
val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId)
findNavController().navigate(action)
}
הניווט מתבצע בגלל קריאה לשיטה navigate. אם הייתם צריכים לכתוב הצהרת assert, אין דרך פשוטה לבדוק אם עברתם אל TaskDetailFragment. הניווט הוא פעולה מורכבת שלא מובילה לתוצאה ברורה או לשינוי במצב, מעבר לאתחול של TaskDetailFragment.
מה שאפשר לקבוע הוא שהשיטה navigate הופעלה עם פרמטר הפעולה הנכון. זה בדיוק מה שכפילת בדיקה מדמה עושה – היא בודקת אם הופעלו שיטות ספציפיות.
Mockito הוא framework ליצירת כפילים לבדיקה. למרות שהמילה mock מופיעה ב-API ובשם, הוא לא משמש רק ליצירת מוקאפים. הוא יכול גם ליצור stubs ו-spies.
תשתמשו ב-Mockito כדי ליצור אובייקט מדומה NavigationController שיוכל לאשר שהמתודה navigate נקראה בצורה נכונה.
שלב 1. הוספת יחסי תלות ב-Gradle
- מוסיפים את יחסי התלות של Gradle.
app/build.gradle
// Dependencies for Android instrumented unit tests
androidTestImplementation "org.mockito:mockito-core:$mockitoVersion"
androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:$dexMakerVersion"
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
-
org.mockito:mockito-core– זוהי תלות ב-Mockito. -
dexmaker-mockito– הספרייה הזו נדרשת כדי להשתמש ב-Mockito בפרויקט Android. Mockito צריך ליצור מחלקות בזמן ריצה. ב-Android, הפעולה הזו מתבצעת באמצעות קוד בייט של dex, ולכן הספרייה הזו מאפשרת ל-Mockito ליצור אובייקטים במהלך זמן הריצה ב-Android. -
androidx.test.espresso:espresso-contrib– הספרייה הזו מורכבת מתכנים חיצוניים (ומכאן השם) שמכילים קוד לבדיקה של תצוגות מתקדמות יותר, כמוDatePickerו-RecyclerView. הוא מכיל גם בדיקות נגישות ומחלקה בשםCountingIdlingResource, שנסביר עליה בהמשך.
שלב 2. יצירת TasksFragmentTest
- פתיחת
TasksFragment. - לוחצים לחיצה ימנית על שם המחלקה
TasksFragmentובוחרים באפשרות יצירה ואז באפשרות בדיקה. יוצרים בדיקה בערכת המקור androidTest. - מעתיקים את הקוד הזה אל
TasksFragmentTest.
TasksFragmentTest.kt
@RunWith(AndroidJUnit4::class)
@MediumTest
@ExperimentalCoroutinesApi
class TasksFragmentTest {
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
}הקוד הזה דומה לקוד TaskDetailFragmentTest שכתבת. הוא מגדיר ומבטל FakeAndroidTestRepository. מוסיפים בדיקת ניווט כדי לבדוק שכאשר לוחצים על משימה ברשימת המשימות, מגיעים לTaskDetailFragment הנכון.
- מוסיפים את הבדיקה
clickTask_navigateToDetailFragmentOne.
TasksFragmentTest.kt
@Test
fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))
// GIVEN - On the home screen
val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
}
- משתמשים בפונקציה
mockשל Mockito כדי ליצור mock.
TasksFragmentTest.kt
val navController = mock(NavController::class.java)כדי ליצור מוק ב-Mockito, מעבירים את המחלקה שרוצים ליצור לה מוק.
בשלב הבא, צריך לשייך את NavController לקטע. onFragment מאפשרת להפעיל שיטות בקטע עצמו.
- הופכים את המוק החדש ל-
NavControllerשל הפרגמנט.
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}- מוסיפים את הקוד ללחיצה על הפריט ב-
RecyclerViewעם הטקסט TITLE1.
// WHEN - Click on the first list item
onView(withId(R.id.tasks_list))
.perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText("TITLE1")), click()))RecyclerViewActions הוא חלק מהספרייה espresso-contrib ומאפשר לכם לבצע פעולות Espresso ב-RecyclerView.
- מוודאים שהפונקציה
navigateנקראה עם הארגומנט הנכון.
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")השיטה verify של Mockito היא מה שהופך את זה ל-mock – אתם יכולים לאשר שה-mock navController קרא לשיטה ספציפית (navigate) עם פרמטר (actionTasksFragmentToTaskDetailFragment עם המזהה id1).
הבדיקה המלאה נראית כך:
@Test
fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))
// GIVEN - On the home screen
val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
val navController = mock(NavController::class.java)
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
// WHEN - Click on the first list item
onView(withId(R.id.tasks_list))
.perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText("TITLE1")), click()))
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
)
}- מריצים את הבדיקה.
לסיכום, כדי לבדוק את הניווט אפשר:
- משתמשים ב-Mockito כדי ליצור
NavControllermock. - מצרפים את ה-mock
NavControllerאל ה-fragment. - מוודאים שהפונקציה navigate הופעלה עם הפעולה והפרמטרים הנכונים.
שלב 3. אופציונלי, כותבים clickAddTaskButton_navigateToAddEditFragment
כדי לבדוק אם אתם יכולים לכתוב בעצמכם בדיקת ניווט, נסו לבצע את המשימה הזו.
- כותבים את הבדיקה
clickAddTaskButton_navigateToAddEditFragmentשבודקת שאם לוחצים על לחצן הפעולה הצף +, עוברים אלAddEditTaskFragment.
התשובה מופיעה בהמשך.
TasksFragmentTest.kt
@Test
fun clickAddTaskButton_navigateToAddEditFragment() {
// GIVEN - On the home screen
val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
val navController = mock(NavController::class.java)
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
// WHEN - Click on the "+" button
onView(withId(R.id.add_task_fab)).perform(click())
// THEN - Verify that we navigate to the add screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToAddEditTaskFragment(
null, getApplicationContext<Context>().getString(R.string.add_task)
)
)
}לוחצים על כאן כדי לראות את ההבדלים בין הקוד שהתחלתם איתו לבין הקוד הסופי.
כדי להוריד את הקוד של ה-codelab המוגמר, אפשר להשתמש בפקודת ה-git הבאה:
$ git clone https://github.com/googlecodelabs/android-testing.git $ cd android-testing $ git checkout end_codelab_2
אפשר גם להוריד את המאגר כקובץ ZIP, לבטל את הדחיסה שלו ולפתוח אותו ב-Android Studio.
בשיעור הזה למדתם איך להגדיר הזרקת תלות ידנית, איתור שירותים ואיך להשתמש ב-fakes וב-mocks באפליקציות Android Kotlin. הקפידו במיוחד על הדברים הבאים:
- מה שרוצים לבדוק ואסטרטגיית הבדיקה קובעים את סוגי הבדיקות שצריך להטמיע באפליקציה. בדיקות יחידה הן ממוקדות ומהירות. בדיקות שילוב מאמתות את האינטראקציה בין חלקי התוכנית. בדיקות מקצה לקצה מאמתות תכונות, הן הכי מדויקות, הן לרוב מנוהלות באמצעות מכשירים ועשויות להימשך זמן רב יותר.
- הארכיטקטורה של האפליקציה משפיעה על מידת הקושי של הבדיקה.
- TDD או פיתוח מונחה בדיקות היא אסטרטגיה שבה כותבים את הבדיקות קודם, ואז יוצרים את התכונה כדי לעבור את הבדיקות.
- כדי לבודד חלקים באפליקציה לצורך בדיקה, אפשר להשתמש ב-test doubles. כפילת בדיקה היא גרסה של מחלקה שנוצרה במיוחד לצורך בדיקה. לדוגמה, אתם מזייפים קבלת נתונים ממסד נתונים או מהאינטרנט.
- משתמשים בהזרקת תלות כדי להחליף מחלקה אמיתית במחלקה לבדיקה, למשל מאגר או שכבת רשת.
- משתמשים בבדיקות עם מכשור (
androidTest) כדי להפעיל רכיבי ממשק משתמש. - כשאי אפשר להשתמש בהזרקת תלות של בנאי, למשל כדי להפעיל fragment, אפשר בדרך כלל להשתמש ב-service locator. התבנית Service Locator היא חלופה ל-Dependency Injection. היא כוללת יצירה של מחלקה יחידה (singleton) שנקראת Service Locator, שמטרתה לספק תלות, גם לקוד הרגיל וגם לקוד הבדיקה.
קורס ב-Udacity:
מסמכי תיעוד למפתחי Android:
- מדריך לארכיטקטורת אפליקציות
- הפקודה
runBlockingוהפקודהrunBlockingTest FragmentScenario- Espresso
- Mockito
- JUnit4
- AndroidX Test Library
- AndroidX Architecture Components Core Test Library
- קבוצות של מקורות
- בדיקה משורת הפקודה
סרטי וידאו:
אחר:
קישורים למדריכי Codelab נוספים בקורס הזה זמינים בדף הנחיתה של מדריכי Codelab בנושא Android מתקדם ב-Kotlin.




