מעבדת קוד זו היא חלק מהקורס המתקדם של Android בקורס Kotlin. כדי להפיק את המקסימום מהקורס הזה, אם עובדים על מעבדות הקוד ברצף, לא חובה לעשות זאת. כל שיעורי הקוד של הקורסים מופיעים בדף הנחיתה המתקדם של Android ב-Kotlin codelabs.
מבוא
מעבדת הקוד השנייה הזו עוסקת בכפולות בדיקה: מתי להשתמש בהן ב-Android ואיך להטמיע אותן באמצעות הזרקת תלות, דוגמת השירות והספריות. כך תלמדו איך לכתוב:
- בדיקות של יחידות מאגר
- מקטעי בדיקה ומודלים של תצוגות מפורטות
- מקטעי ניווט מובנים
דברים שחשוב לדעת
כדאי שתכירו את:
- שפת התכנות Kotlin
- בדיקת קונספטים המכוסים ב-Codelab הראשון: כתיבה והפעלה של בדיקות יחידה ב-Android, באמצעות JUNIT, Humcrest, בדיקת AndroidX, Robolectric, וגם בדיקת LiveData
- הספריות המרכזיות של Android Jetpack הבאות:
ViewModel
,LiveData
ורכיב הניווט - ארכיטקטורת אפליקציות, לפי הדפוסים המפורטים במדריך לארכיטקטורת אפליקציות וב-Android Fundamentals codelabs
- העקרונות הבסיסיים של שגרות ב-Android
מה תלמדו
- איך לתכנן אסטרטגיית בדיקה
- איך ליצור ולהשתמש בכפולות בדיקה, כלומר זיוף והדמיה
- איך להשתמש בהזרקת תלות ידנית ב-Android לבדיקות של יחידות ושילוב
- איך מחילים את הדפוס של ממקם השירות
- איך לבדוק מאגרים, מקטעים, מקטעים של תצוגה ומודלים של ניווט
תשתמשו בספריות ובקונספטים הבאים של הקוד:
הפעולות שתבצעו:
- כותבים בדיקות יחידה למאגר באמצעות הזרקה כפולה של הזרקה ותלויות.
- כותבים בדיקות יחידה למודל תצוגה באמצעות הזרקת בדיקות כפולה והזרקת תלות.
- כתיבת בדיקות שילוב עבור מקטעים ומודלים של תצוגה באמצעות מסגרת בדיקת ממשק משתמש Espresso.
- כתיבת בדיקות ניווט באמצעות Mockito ו-Espresso.
בסדרה של משימות Lab אלה, אתם תעבדו עם אפליקציית ההערות לביצוע. האפליקציה מאפשרת לכתוב משימות כדי להשלים אותן ולהציג אותן ברשימה. תוכלו לסמן אותם כ'בוצעו' או 'לא בוצעו', לסנן אותם או למחוק אותם.
האפליקציה הזו כתובה ב-Kotlin, היא כוללת מספר מסכים, משתמשת ברכיבי Jetpack ועוקבת אחר הארכיטקטורה מתוך מדריך לארכיטקטורת אפליקציות. אם תבדקו איך לבדוק את האפליקציה הזו, תוכלו לבדוק אפליקציות שמשתמשות באותן ספריות ובארכיטקטורה.
להורדת הקוד
כדי להתחיל, יש להוריד את הקוד:
לחלופין, אפשר לשכפל את מאגר GitHub לקוד:
$ git clone https://github.com/googlecodelabs/android-testing.git $ cd android-testing $ git checkout end_codelab_1
כדאי להקדיש רגע ולהכיר את הקוד לפי ההוראות הבאות.
שלב 1: מריצים את האפליקציה לדוגמה
אחרי שמורידים את האפליקציה 'רשימת משימות', פותחים אותה ב-Android Studio ומפעילים אותה. הוא אמור להידור. כדי לבחון את האפליקציה, מבצעים את הפעולות הבאות:
- יצירת משימה חדשה באמצעות לחצן הפעולה הצף. קודם מזינים כותרת ולאחר מכן מזינים מידע נוסף לגבי המשימה. שומרים אותו עם ההמחאה הירוקה (FAB).
- ברשימת המשימות, לוחצים על השם של המשימה שסיימתם ומעיינים במסך של המשימה כדי לראות את שאר התיאור.
- ברשימה או במסך הפרטים, מסמנים את התיבה של המשימה כדי להגדיר את הסטטוס שלה להושלם.
- חוזרים למסך המשימות, פותחים את תפריט הסינון ומסננים את המשימות לפי הסטטוס פעיל וסטטוס הושלם.
- פותחים את חלונית ההזזה לניווט ולוחצים על נתונים סטטיסטיים.
- חזרה למסך הסקירה הכללית, ובתפריט חלונית ההזזה לניווט, בוחרים באפשרות ניקוי שהושלמו כדי למחוק את כל המשימות עם הסטטוס הושלם.
שלב 2: עיון בקוד האפליקציה לדוגמה
אפליקציית TO-DO מבוססת על דגימת הבדיקה הפופולרית של ארכיטקטורת ארכיטקטורה (באמצעות גרסת הארכיטקטורה רספונסיבית של הדוגמה). האפליקציה בנויה לפי הארכיטקטורה במדריך לארכיטקטורת אפליקציות. המודל משתמש ב-ViewModel עם Fragments, מאגר וחדר. אם אתם מכירים את אחת מהדוגמאות הבאות, לאפליקציה הזו יש ארכיטקטורה דומה:
- חדר עם View Code Lab
- מעבדות קוד הדרכה ל-Android Kotlin Fundamentals
- שיעורי Lab להדרכה ב-Android
- דוגמה ל-Android החמנייה
- פיתוח אפליקציות ל-Android באמצעות קורס ההדרכה של Kotlin Udacity
חשוב יותר להבין את הארכיטקטורה הכללית של האפליקציה מאשר להבין לעומק את הלוגיקה בכל שכבה.
זהו סיכום החבילות שתמצא:
חבילה: | |
| הוספה או עריכה של מסך משימה: קוד שכבה של ממשק משתמש להוספה או לעריכה של משימה. |
| שכבת הנתונים: פעולה זו מתאימה לשכבת הנתונים של המשימות. הם כוללים את מסד הנתונים, הרשת וקוד המאגר. |
| מסך הנתונים הסטטיסטיים: קוד שכבה של ממשק משתמש למסך הנתונים הסטטיסטיים. |
| מסך פרטי המשימה: קוד שכבה של ממשק משתמש למשימה אחת. |
| מסך המשימות: קוד שכבת ממשק המשתמש עבור הרשימה של כל המשימות. |
| שיעורי עזר: כיתות משותפות המשמשות בחלקים שונים של האפליקציה. למשל, פריסת ההחלקה המשמשת במספר מסכים. |
שכבת נתונים (.data)
אפליקציה זו כוללת סימולציה של שכבת רשת, בחבילה שלט רחוק, ושכבת מסד נתונים בחבילה מקומית. כדי לפשט את הדברים, בפרויקט הזה מתבצעת סימולציה של שכבת הרשת עם HashMap
בלבד, עם עיכוב במקום ביצוע בקשות אמיתיות ברשת.
הקואורדינטות של DefaultTasksRepository
מתווכות או מתווכות בין שכבת הרשת לבין שכבת מסד הנתונים, והיא זו שמחזירה נתונים לשכבת ממשק המשתמש.
שכבת ממשק משתמש ( .addedittask, .statistics, .taskdetail, .tasks)
כל אחת מהחבילות של שכבות ממשק המשתמש מכילה מקטע ומודל תצוגה, וכן כיתות אחרות הנדרשות עבור ממשק המשתמש (כגון מתאם עבור רשימת המשימות). הTaskActivity
הוא הפעילות שמכילה את כל הקטעים.
ניווט
הניווט באפליקציה נשלט על ידי רכיב הניווט. הוא מוגדר בקובץ nav_graph.xml
. הניווט מופעל במודלים של תצוגות מפורטות באמצעות הכיתה Event
; המודלים של התצוגה קובעים גם אילו ארגומנטים יעברו. המקטעים מעיינים ב-Event
ומבצעים את הניווט בפועל בין מסכים.
במעבדת קוד זו תלמדו איך לבדוק מאגרים, להציג מודלים ומקטעים באמצעות עותקים כפולים של בדיקות והזרקת תלות. לפני שמתחילים לחקור לעומק את הנושאים האלה, חשוב להבין את הסיבה שתנחה אתכם ואת הכתיבה של הבדיקות האלה.
הקטע הזה מפרט כמה שיטות מומלצות לבדיקה באופן כללי, כפי שהן רלוונטיות ל-Android.
פירמידת הבדיקה
כשחושבים על שיטת בדיקה, יש שלושה היבטים הקשורים לבדיקה:
- היקף – באיזה חלק של הקוד הבדיקה מתייחסת? הבדיקות יכולות לפעול בשיטה אחת, באפליקציה כולה או במקום כלשהו ביניהן.
- מהירות – באיזו מהירות הבדיקה מופעלת? מהירויות הבדיקה יכולות להשתנות מאלפיות שנייה לכמה דקות.
- אמינות – איך "המציאות לדוגמה, אם חלק מהקוד שצריך לבדוק הוא לבצע בקשת רשת, האם קוד הבדיקה הוא למעשה שולח את בקשת הרשת הזו, או שהוא מזויף את התוצאה? אם בפועל הבדיקה היא ברשת, המשמעות היא שהאיכות שלה גבוהה יותר. החיסרון הוא שהבדיקה עשויה להימשך זמן רב יותר, והיא עלולה לגרום לשגיאות אם הרשת מושבתת או שהשימוש בה עלול להיות יקר.
קיימים פשרות מהותיות בין ההיבטים האלה. לדוגמה, המהירות והאמינות הן האיזון הנכון – בדרך כלל הבדיקה מהירה יותר, ולהיפך. אחת מהדרכים הנפוצות ביותר לחלק בדיקות אוטומטיות היא לשלוש הקטגוריות האלה:
- בדיקות יחידה – בדיקות ממוקדות מאוד שפועלות בכיתה אחת, בדרך כלל באותה שיטה. אם בדיקת יחידה תיכשל, תוכלו לדעת בדיוק היכן נמצאת הבעיה בקוד. האיכות שלהן נמוכה מאוד, מאחר שבעולם האמיתי, האפליקציה שלכם כוללת הרבה יותר מביצוע של שיטה אחת או מחלקה אחת. הם מהירים מספיק כדי לפעול בכל פעם שאתה משנה את הקוד. לרוב, הן יעברו בדיקות מקומיות (בקבוצת המקור של
test
). דוגמה: בדיקת שיטות יחיד במודלים ובמאגרי תצוגות. - בדיקות שילוב — הבדיקות האלה מאפשרות לבחון את האינטראקציה של כמה כיתות כדי לוודא שהן פועלות כמצופה כשמשתמשים בהן יחד. אחת הדרכים לבנות בדיקות שילוב היא לבקש שהן יבדקו תכונה אחת, כמו היכולת לשמור משימה. הם בודקים טווח רחב יותר של קוד מאשר בדיקות יחידה, אבל הם עדיין מותאמים להפעלה מהירה, לעומת אמינות מלאה. ניתן להריץ אותן באופן מקומי או כבדיקות אינסטרומנטציה, בהתאם למצב. דוגמה: בדיקה של כל הפונקציונליות של שבר יחיד ושל זוג מודלים של תצוגה.
- בדיקות מקצה לקצה (E2e) – כדאי לבדוק שילוב של תכונות שפועלות יחד. הם בודקים חלקים גדולים של האפליקציה, מדמים שימוש אמיתי מקרוב, ולכן בדרך כלל הם איטיים. רמת המהימנות הגבוהה ביותר היא שמספר האפליקציות שלך אכן פועל במתכונת כוללת. בגדול, הבדיקות האלה יהיו בדיקות אינסטרומנטטיביות (בקבוצת המקור של
androidTest
)
דוגמה: הפעלה של האפליקציה כולה ובדיקה של כמה תכונות יחד.
לרוב, שיעור ההצעות של הבדיקות האלה מיוצג על ידי פירמידה, ורוב הבדיקות הן יחידות יחידה.
ארכיטקטורה ובדיקה
היכולת שלך לבדוק את האפליקציה בכל הרמות השונות של פירמידת הבדיקה קשורה באופן מהותי לארכיטקטורה של האפליקציה. לדוגמה, אפליקציה שקשה מאוד להשתמש בה, עם מבנה אדריכלי שגוי עלולה לכלול את כל הלוגיקה שלה בשיטה אחת. ייתכן שתוכלו לכתוב בדיקה מקצה לקצה עבור הבדיקה הזו, כי ברוב המקרים הבדיקות האלה בודקות חלקים גדולים מהאפליקציה, אבל מה לגבי כתיבת יחידות ניסוי או שילוב? כשכל הקוד מרוכז במקום אחד, קשה לבדוק רק את הקוד שקשור ליחידה או לתכונה אחת.
גישה טובה יותר היא לחלק את הלוגיקה של האפליקציה למספר שיטות וכיתות, כך שכל בדיקה תבודד. ארכיטקטורה היא דרך לחלק את הקוד ולארגן אותו, וכך קל יותר לבדוק יחידות ושילובים. אפליקציית TO-Do שעליך לבדוק תואמת לארכיטקטורה מסוימת:
בשיעור זה תלמדו איך לבדוק חלקים מהארכיטקטורה שלמעלה בנפרד, בנפרד
- תחילה עליכם לבדוק את היחידה של המאגר.
- לאחר מכן תשתמשו בכפולה בדיקה של מודל המודלים של התצוגה המפורטת, הנדרשת לבדיקת יחידה ולבדיקת שילוב של מודל התצוגה המפורטת.
- בשלב הבא, תלמדו איך לכתוב בדיקות שילוב עבור מקטעים ומודלים של התצוגה המפורטת שלהם.
- לבסוף, תלמדו איך לכתוב בדיקות שילוב שכוללות את רכיב הניווט.
בדיקת קצה-קצה תכסה בשיעור הבא.
כשכותבים בדיקת יחידה לחלק מכיתה (שיטה או אוסף קטן של שיטות), המטרה היא לבדוק רק את הקוד של הכיתה.
לא מספיק לבדוק רק קוד בכיתה או בכיתות מסוימים. נבחן את הדוגמה הבאה. פותחים את הכיתה 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
היא אחת מהשיחות הנפוצות ביותר "basic&PLURAL; שניתן לבצע במאגר הנתונים שלכם. שיטה זו כוללת קריאה ממסד נתונים של SQLite וביצוע קריאות רשת (הקריאה ל-updateTasksFromRemoteDataSource
). התהליך הזה דורש הרבה יותר קוד מאשר רק קוד המאגר.
לפניכם כמה סיבות ספציפיות יותר לכך שקשה לבדוק את המאגר:
- כדי לבצע אפילו את הבדיקות הפשוטות ביותר עבור המאגר הזה, צריך לחשוב על יצירה וניהול של מסד נתונים. יופיעו שאלות כמו "אם זו צריכה להיות בדיקה מקומית או אינסטרומנטלית על בדיקה; ואם צריך להשתמש בבדיקת AndroidX כדי לקבל הדמיה של סביבת Android.
- חלקים מסוימים של הקוד, כגון קוד רשת, עשויים להימשך זמן רב או עד שבמקרים מסוימים, הם יוצרים בדיקות ארוכות ומושכות.
- ייתכן שהבדיקות יקבעו שאין להן קוד כדי לאבחן איזה קוד כושל בבדיקה. ייתכן שהבדיקות יתחילו לבדוק קוד שאינו מאגר, כך למשל, בדיקת ה-"repository" שלכם עשויה להיכשל עקב בעיה בחלק מהקוד התלוי, כמו קוד מסד הנתונים.
בדיקות כפולות
הפתרון לכך הוא שכשאתם בודקים את המאגר, אל תשתמשו בקוד האמיתי של רשת או מסד נתונים, אלא במקום זאת השתמשו במכפיל בדיקה. כפולה של בדיקות היא גרסה של כיתה שנוצרה במיוחד לבדיקה. היא נועדה להחליף את הגרסה האמיתית של כיתה בבדיקות. היא דומה לאופן שבו כפילות פעלולים היא שחקן המתמחה בפעלולים, והיא מחליפה את השחקן האמיתי בפעולות מסוכנות.
הנה כמה סוגים של כפילויות בבדיקות:
זיוף | הכפלה של ניסיון להטמעה של "working"E; |
הדמיה | כפולה ניסיונית שעוקבת אילו מהשיטות שלה נקראו. לאחר מכן היא עוברת את הבדיקה או כושלת, בהתאם לשיטת הקריאה שלה. |
מקפצה | כפל בדיקה שאינו כולל לוגיקה ומחזיר רק את מה שאתם מתכננים להחזיר. לדוגמה, אפשר לתכנת |
דמה | כפולה מבוטא שמועברת אך לא בשימוש, למשל, רק צריך לספק אותה כפרמטר. אם היה לך |
ריגול | כפולה של בדיקה, שגם עוקבת אחר מידע נוסף נוסף. לדוגמה, אם ביצעת |
מידע נוסף על הכפלות בבדיקה זמין במאמר בדיקות בשירותים: בדקו את כפולה שלך.
כפול המצבים הנפוצים ביותר בבדיקה ב-Android הם זיוף והדמיה.
במשימה הזו, בכוונתך ליצור בדיקה כפולה של 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 לא הטמעת את השיטות הנדרשות עבור TasksDataSource
.
- משתמשים בתפריט התיקון המהיר ובוחרים באפשרות הטמעת חברים.
- בוחרים את כל השיטות ולוחצים על אישור.
שלב 3: הטמעת שיטת getTasks ב-FakeDataSource
FakeDataSource
הוא סוג ספציפי של מכפיל בדיקה שנקרא זיוף. מזויף הוא העתק כפול לבדיקה, שההטמעה שלו כוללת גם את יישום הכיתוב אבל הוא מיושם באופן שמתאים לבדיקות, אך אינו מתאים לייצור. "Working" המשמעות היא שהכיתה תיצור פלט מציאותי בהינתן קלט.
לדוגמה, מקור הנתונים המזויף לא יתחבר לרשת ולא ישמור שום דבר במסד נתונים, אלא רק ישתמש ברשימת זיכרון. הפעולה הזו&תפעל; כפי שציפיתם; &בשיטות האלה, כדי לקבל או לשמור משימות יוחזרו תוצאות צפויות, אך אף פעם לא תוכלו להשתמש בהטמעה הזו בסביבת ייצור כי היא לא נשמרת בשרת או במסד נתונים.
תמריץ FakeDataSource
- מאפשר לך לבדוק את הקוד ב-
DefaultTasksRepository
מבלי להסתמך על מסד נתונים או רשת אמיתיים. - מספק "real-eallow" לצורך בדיקות.
- יש לשנות את המבנה של
FakeDataSource
כדי ליצורvar
בשםtasks
שהואMutableList<Task>?
עם ערך ברירת מחדל של רשימה ריקה.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }
זו רשימת המשימות ש&מירכאות, מירכאות ופירושן מסד נתונים או תגובה של שרת. בשלב הזה, המטרה היא לבדוק את השיטה של המאגר getTasks
. כך ניתן להשתמש בשיטות מקור הנתונים& getTasks
, deleteAllTasks
ו-saveTask
.
יש לכתוב גרסה מזויפת של השיטות הבאות:
- כותבים
getTasks
: אםtasks
לאnull
, מחזירים תוצאהSuccess
. אם הערך של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
אופן הפעולה הזה דומה לאופן הפעולה של מקורות נתונים מקומיים ומרוחקים בפועל.
בשלב זה, אתם משתמשים בשיטה שנקראת הזרקת תלות ידנית, כדי שתוכלו להשתמש בכפולה מזויפת לבדיקה שיצרתם.
הבעיה העיקרית היא שיש לך 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
, לכן הוא מכיל קוד בתוך הקוד. אין אפשרות להחליף את הפריט בכפולת הבדיקה שלך.
במקום זאת, תספקו את מקורות הנתונים האלה לכיתה, במקום לסמן אותם בקוד קשיח. מתן תלות נקרא הזרקת תלות. יש דרכים שונות לספק תלות, ולכן סוגים שונים של הזרקת תלות.
הזרקת תלות של קבלן מאפשרת לכם להחליף בכפולת הבדיקה על ידי העברתה לבנאי.
ללא הזרקה | הזרקה |
שלב 1: שימוש בהזרקת תלות של קבלן ב-DefaultTasksRepository
- יש לשנות את ה-constructor של
DefaultTaskRepository
מ-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
. אין יותר צורך ליצור תלויות. - למחוק גם את משתני המופע הישנים. אתה מגדיר אותם בבונה:
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
בקבוצת המקור בדיקה. - בחלק העליון של סיווג
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: כתיבת ברירת המחדל של Tasks(Tasks) 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: הוספת RunBlockTest
צפויה שגיאת coroutine כי הפונקציה getTasks
היא פונקציה ב-suspend
ואתם צריכים להפעיל שגרת נתונים כדי להפעיל אותה. לשם כך, תצטרכו היקף ברמת קורטה. כדי לפתור את השגיאה הזו, תצטרכו להוסיף משתנים תלויים לטיפול בהפעלת שגרת שגרה בבדיקות.
- על ידי שימוש ב-
testImplementation
אפשר להוסיף את הגורמים התלויים הדרושים לבדיקה של קוראונים.
app/build.gradle
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
לא לשכוח לסנכרן!
kotlinx-coroutines-test
היא ספריית בדיקות הקורינתן, שמיועדת במיוחד לבדיקת קוראוטן. כדי להריץ את הבדיקות, צריך להשתמש בפונקציה runBlockingTest
. פונקציה זו מסופקת על ידי ספריית הבדיקה של coroutines. היא מקבלת קטע קוד ולאחר מכן מפעילה את בלוק הקוד הזה בהקשר יוצא דופן של קורונה, שפועל באופן סינכרוני ומיידי. כלומר, פעולות יתרחשו בסדר יורד. באופן כללי, המטרה היא שהקורינות שלך יפעלו כמו שגרתיים, ולכן היא מיועדת לבדיקת קוד.
צריך להשתמש בפונקציה runBlockingTest
בכיתות הבדיקה כשמתקשרים לפונקציה suspend
. בהסבר נוסף על אופן הפעולה של 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
כדי לוודא שהיא פועלת והשגיאה נעלמה!
הרגע ראיתם איך מקבצים בדיקה של מאגר. בשלבים הבאים, עליך להשתמש שוב בהזרקת תלות וליצור פעולת בדיקה נוספת – הפעם כדי להראות איך לכתוב בדיקות של יחידה ושילוב עבור המודלים של התצוגה המפורטת שלך.
בדיקות היחידה עליך לבדוק רק את הכיתה או השיטה שבה אתה מעוניין. פעולה זו נקראת 'בדיקה' בבידוד, שבו יתבצע בידוד ברור של ה-"unit" ונבדק רק הקוד שהוא חלק מהיחידה הזו.
לכן, TasksViewModelTest
צריך לבדוק רק את קוד TasksViewModel
— אין לבדוק אותו במסד הנתונים, ברשת או במחלקות המאגר. לכן, עבור המודלים של התצוגה המפורטת, בדומה למה שעשיתם עכשיו עבור המאגר, אתם תיצרו מאגר מזויף ותחילו התלויות כדי להשתמש בו בבדיקות.
במשימה הזו תטמיעו מודלים תלויים כדי להציג מודלים.
שלב 1. יצירת ממשק Repository של Tasks
השלב הראשון בשימוש בהזרקת תלות של קבלן הוא יצירת ממשק משותף בין המצב המזויף לכיתה בפועל.
איך זה נראה בפועל? יש לבחון את 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
, כפי שעשית עבור מקורות הנתונים. עליה לכלול את כל השיטות הגלויות לכול (פני ה-API הציבוריים) של DefaultTasksRepository
.
- פותחים את
DefaultTasksRepository
ולוחצים לחיצה ימנית על שם הכיתה. לאחר מכן בוחרים באפשרות גורם חדש -> חילוץ -> ממשק.
- בוחרים באפשרות חילוץ כדי להפריד בין קבצים.
- בחלון API של חילוץ משנים את שם הממשק ל-
TasksRepository
. - בקטע ממשק ליצירת חברים, מסמנים את כל החברים מלבד שני החברים הנלווים והשיטות הפרטיות.
- לוחצים על מדידה מחדש. ממשק
TasksRepository
החדש אמור להופיע בחבילה של data/source .
כמו כן, DefaultTasksRepository
מטמיע עכשיו את TasksRepository
.
- מפעילים את האפליקציה (לא את הבדיקות) כדי לוודא שהכול פועל כמו שצריך.
שלב 2. יצירת FakeTestRepository
עכשיו יש לך ממשק, ואפשר ליצור כפול של הבדיקה. DefaultTaskRepository
- במקור הבדיקה, בנתונים/מקור יוצרים את קובץ Kotlin ומחלקה
FakeTestRepository.kt
ומרחיבים אותו מהממשק שלTasksRepository
.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
}
תקבלו הנחיות להטמיע את שיטות הממשק.
- מעבירים את העכבר מעל השגיאה עד שרואים את תפריט ההצעות, ואז לוחצים על צירוף משתמשים ובוחרים בהם.
- בוחרים את כל השיטות ולוחצים על אישור.
שלב 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. הוספת שיטה לבדיקה כדי להוסיף אותה ל-Tasks
בעת הבדיקה, עדיף שיהיו כבר כמה 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
, שיוצר את מודל התצוגה המפורטת. כדי לשנות את אופן היצירה של מודל התצוגה, צריך להוסיף ViewModelProvider.Factory
ולהשתמש בו. אם אינכם מכירים את ViewModelProvider.Factory
, תוכלו לקבל מידע נוסף בנושא כאן.
שלב 1. יצירה של מפעל ViewView ב-TasksViewModel ושימוש בו
אפשר להתחיל לעדכן את הכיתות ולבדוק את מה שקשור למסך Tasks
.
- פתוח
TasksViewModel
. - צריך לשנות את המבנה של
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
}
מכיוון ששינית את הבונה, עליך להשתמש כעת במפעל כדי לבנות את 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
. עכשיו, אחרי שיצרתם את המפעל, תוכלו להשתמש בו בכל מקום שבו אתם בונים את מודל התצוגה המפורטת.
- יש לעדכן את
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 Testing
ApplicationProvider.getApplicationContext
, אפשר גם להסיר את ההערה@RunWith(AndroidJUnit4::class)
. - כדאי להריץ את הבדיקות כדי לוודא שהן עדיין פועלות!
על ידי הזרקה של תלות בבונה, הסרת את הDefaultTasksRepository
כתלויה והחלפת אותו ב-FakeTestRepository
בבדיקות.
שלב 3. עדכון גם מקטע TaskDetail ו-ViewModel
יש לבצע את אותם שינויים ב-TaskDetailFragment
וב-TaskDetailViewModel
. הפעולה הזו תכין את הקוד לקריאה הבאה של TaskDetail
בדיקות.
- פתוח
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
.
בשלב הבא, יש לכתוב בדיקות שילוב כדי לבדוק את האינטראקציות בין חלקי הקוד והמודל. תוכלו לבדוק אם קוד הדגם שלכם מתעדכן כראוי בממשק המשתמש. כדי לעשות את זה
- תבנית ה-ServiceLocator
- ספריות אספרסו ומוקיטו
בדיקות שילוב בודקים את האינטראקציה בין כמה כיתות כדי לוודא שהן פועלות כמצופה כשמשתמשים בהן יחד. אפשר להריץ את הבדיקות האלה באופן מקומי (קבוצת מקורות אחת (test
) או כבדיקות אינסטרומנטציה (קבוצת מקורות מסוג androidTest
).
במקרה שלך עליך לבצע בדיקה של כל מקטע ולכתוב בדיקות שילוב של החלק והתצוגה כדי לבדוק את התכונות העיקריות של המקטע.
שלב 1. הוספת יחסי תלות של שולי האזור
- יש להוסיף את הציונים הבאים.
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
— ספריית בדיקות ליבה של AndroidXkotlinx-coroutines-test
– ספריית בדיקות הקורינתandroidx.fragment:fragment-testing
— ספריית בדיקות AndroidX ליצירת מקטעים בבדיקות ושינוי המצב שלהם.
מאחר שהספריות האלה יהיו בשימוש בקבוצת המקור שלך ב-androidTest
, עליך להשתמש ב-androidTestImplementation
כדי להוסיף אותן כתלויות.
שלב 2. יצירת סיווג של TaskDetailFragmentTest
בTaskDetailFragment
מוצג מידע על משימה אחת.
אפשר להתחיל בדיקת קטע של TaskDetailFragment
כי יש לו פונקציונליות בסיסית למדי בהשוואה למקטעים אחרים.
- פתוח
taskdetail.TaskDetailFragment
. - יוצרים בדיקה עבור
TaskDetailFragment
, כפי שעשיתם בעבר. עליך לבחור את אפשרויות ברירת המחדל ולהציב אותן בקבוצת המקורות androidTest (לא בקבוצת המקורtest
).
- מוסיפים את ההערות הבאות לכיתה
TaskDetailFragmentTest
.
TaskDetailFragmentTest.kt
@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
}
מטרת ההערה:
@MediumTest
– סימון הבדיקה כ"זמן ריצה וזמן ריצה;;; בדיקת שילוב כך תוכלו לקבץ ולבחור את גודל הבדיקה להרצה.@RunWith(AndroidJUnit4::class)
—משמש בכל כיתה באמצעות בדיקת AndroidX.
שלב 3. הפעלת קטע קוד מבדיקה
במשימה הזו, בכוונתך להפעיל את TaskDetailFragment
באמצעות ספריית הבדיקה של AndroidX. FragmentScenario
היא כיתה מבדיקת AndroidX המקיפה שבר ומעניקה לכם שליטה ישירה במחזור החיים של המקטע. כדי לכתוב בדיקות עבור מקטעים, צריך ליצור 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
. כך תוכלו לכתוב את קטע הקוד ולהציג בדיקות של שילוב מודלים.
לא ניתן להשתמש כאן בהזרקת תלות של קבלן, כפי שעשיתם קודם לכן, כשצריך לספק מודל למודל או למאגר. הזרקת תלות של מבנה המבנה מחייבת יצירה של הכיתה. מקטעי פעילויות ופעילויות הם דוגמאות לכיתות שאינך בונה ובדרך כלל אין לך גישה לבנייה שלהן.
מכיוון שאינך בונה את המקטע, אינך יכול להשתמש בהזרקת תלות של מבנה המבנה כדי להחליף אותו בכפולה של בדיקת מאגר (FakeTestRepository
). במקום זאת, יש להשתמש בתבנית מאתר שירות. דפוס ממקם השירות הוא חלופה להזרקת תלות. היא כרוכה ביצירת סיווג יחיד הנקרא "Service Locator" ומטרתו היא לספק תלות, הן עבור הקוד הרגיל והן עבור קוד הבדיקה. בקוד האפליקציה הרגיל (קבוצת המקור main
), כל התלויות האלה הן תלויות האפליקציה הרגילות. בבדיקות, אתם משנים את ממקם השירות על מנת לספק גרסאות כפולות של תלויות.
לא משתמש במאתר השירותים | מאתר שירותים |
עבור האפליקציה הזו של Codelab, יש לבצע את הפעולות הבאות:
- יצירה של סיווג ממקם שירות שיכול לבנות ולאחסן מאגר. כברירת מחדל, הוא יוצר מאגר "regular" .
- מגדירים מחדש את הקוד כך שכאשר יהיה צורך במאגר, השתמשו בממקם השירות.
- בשיעור הבדיקה, קוראים לשיטה בממקם השירות שמחליפה את מאגר הכתובות "normal" בכפולה של הבדיקה.
שלב 1. יצירה של ServiceLocator
קדימה, מתחילים כיתה של ServiceLocator
. הוא ישותף בקוד המקור שהוגדר יחד עם שאר קוד האפליקציה, מכיוון שהוא משמש בקוד האפליקציה הראשי.
הערה: ServiceLocator
היא יחידה, לכן צריך להשתמש במילת המפתח Kotlin object
לכיתה.
- יוצרים את הקובץ ServiceLocator.kt ברמה העליונה של קבוצת המקור הראשית.
- הגדרה של
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
.
חשוב ליצור רק מופע אחד של סיווג מאגר הנתונים. כדי לוודא זאת, עליך להשתמש בממקם השירות בכיתה שלי.
- ברמה העליונה של היררכיית החבילות, פותחים את
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. יצירת 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.kt
. - סימון המגדיר של
tasksRepository
בתור@VisibleForTesting
. הערה זו מאפשרת לבטא את הסיבה שבגללה מגדירים את הציבור היא בגלל בדיקה.
ServiceLocator.kt
@Volatile
var tasksRepository: TasksRepository? = null
@VisibleForTesting set
גם אם אתם מריצים את הבדיקה בלבד וגם אם מדובר בקבוצת בדיקות, הבדיקות צריכות להיות זהות. המשמעות היא שהבדיקות לא צריכות להיות התנהגות שתלויות זו בזו (כלומר, הימנעות משיתוף אובייקטים בין בדיקות).
מאחר שה-ServiceLocator
הוא יחידה, ניתן לשתף אותו בטעות בין בדיקות. כדי למנוע זאת, כדאי ליצור שיטה שתאפס בצורה נכונה את מצב 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()
.
בדומה למה שהיה קודם, המקטע אמור להופיע, מלבד הפעם, מכיוון שהגדרתם כראוי את המאגר, עכשיו הוא מציג את פרטי המשימה.
בשלב הזה, עליך להשתמש בספריית הבדיקה של אספרסו כדי להשלים את בדיקת השילוב הראשונה. בנייתם את הקוד כדי שתוכלו להוסיף בדיקות עם הצהרות לגבי ממשק המשתמש. לשם כך, עליך להשתמש בספריית הבדיקות של אספרסו.
אספרסו עוזר לכם:
- אינטראקציה עם צפיות, כמו לחיצה על לחצנים, הזזה של סרגל או גלילה למטה במסך.
- הצהרתם שצפיות מסוימות מופיעות על המסך או שהן במצב מסוים (למשל, טקסט מסוים או שתיבת סימון מסומנת וכו').
שלב 1. תלות בשוליים
כבר יש לך את התלות הראשית של אספרסו כי היא כלולה בפרויקטים של Android כברירת מחדל.
app/build.gradle
dependencies {
// ALREADY in your code
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
// Other dependencies
}
androidx.test.espresso:espresso-core
– התלות העיקרית של אספרסו כלולה כברירת מחדל כשיוצרים פרויקט חדש ל-Android. הוא מכיל את קוד הבדיקה הבסיסי עבור רוב הצפיות והפעולות הקשורות אליו.
שלב 2. השבתת האנימציות
בדיקות אספרסו פועלות במכשיר אמיתי ולכן הן בדיקות אינסטרומנטציה שמבוצעות בטבע. אחת מהבעיות שצצות היא אנימציות: אם אנימציה נעצרת ואתם מנסים לבדוק אם תצוגה מסוימת מוצגת במסך, אבל היא עדיין מבצעת אנימציה, אספרסו עלול להיכשל בבדיקה. פעולה זו עלולה לגרום לבדיקות אספרסו רעועות.
בבדיקה של ממשק המשתמש של אספרסו, השיטה המומלצת היא לכבות את האנימציות (גם הבדיקה שלך תרוץ מהר יותר!):
- במכשיר הבדיקה, עוברים אל הגדרות > אפשרויות למפתחים.
- משביתים את שלוש ההגדרות האלה: קנה מידה של אנימציה של חלון, קנה מידה של אנימציה במעבר וטווח משך אנימציה.
שלב 3. בדיקה של אספרסו
לפני שכותבים בדיקת אספרסו, אפשר לעיין בקוד של אספרסו.
onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))
בהצהרה הזו מופיעה תצוגת תיבת הסימון עם המזהה task_detail_complete_checkbox
, לוחצים עליה ואז היא מאשרת שהיא מסומנת.
רוב הצהרות אספרסו מורכבות מארבעה חלקים:
onView
onView
היא דוגמה לשיטה של אספרסו סטטי שמתחילה בהצהרת אספרסו. 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
. ViewAssertion
בודקים או מצהירים על משהו בנוגע לתצוגה. ההצהרת ViewAssertion
הנפוצה ביותר שבה תשתמשו היא הטענה matches
. כדי לסיים את ההצהרה, יש להשתמש בViewMatcher
אחר, במקרה הזה isChecked
.
לתשומת ליבך, לא תמיד להתקשר אל perform
וגם אל check
בהצהרת אספרסו. אפשר להשתמש בהצהרות שמבצעות רק הצהרה באמצעות 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
כולל אספרסו. יש לבדוק את מבנה הבדיקה ואת השימוש ב-withId
כדי לבדוק איך דף הפרטים אמור להיראות. - מריצים את הבדיקה ומוודאים שהיא עברה.
שלב 4. אופציונלי: כותבים בדיקת אספרסו משלכם
עכשיו אתם יכולים לכתוב בדיקה בעצמכם.
- יצירת בדיקה חדשה בשם
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()))
}
בשלב האחרון תלמדו איך לבדוק את רכיב הניווט באמצעות סוג אחר של שכפול בדיקה בשם הדמיה, וספריית הבדיקה Mockito.
ב-codelab זה השתמשתם בכפולה בדיקה בשם זיוף. סוגיית הזיוף היא אחד מסוגי התשלום הכפולים. באיזו בדיקה כפולה צריך להשתמש כדי לבדוק את רכיב הניווט?
חשבו איך הניווט מתבצע. נניח שלחיצה על אחת המשימות בקובץ TasksFragment
כדי לנווט למסך של פרטי המשימה.
זהו הקוד ב-TasksFragment
שמנווט למסך של פרטי משימה כשלוחצים עליו.
TasksFragment.kt
private fun openTaskDetails(taskId: String) {
val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId)
findNavController().navigate(action)
}
הניווט מתבצע עקב קריאה לשיטה navigate
. אם היית צריך לכתוב הצהרה, אין דרך פשוטה לבדוק אם ניווטת אל TaskDetailFragment
. הניווט הוא פעולה מורכבת שלא מובילה לפלט ברור או לשינוי במדינה, מעבר להפעלת TaskDetailFragment
.
אפשר לטעון שהשיטה navigate
הופעלה באמצעות פרמטר הפעולה הנכון. זה בדיוק מה שכפולה של בדיקה מתבצעת – היא בודקת אם שיטות ספציפיות נקראו.
Mockito היא מסגרת לייצור כפולים. המילה הדמיית משמשת גם ב-API ובשם שלה, אבל לא היא רק הדמיה. אפשר גם להשתמש ב ארובה לרגליים ומרגלים.
עליך להשתמש ב-Mockito כדי ליצור הדמיה של NavigationController
שניתן להצהיר ששיטת הניווט נקראה כראוי.
שלב 1. הוספת יחסי תלות של שולי האזור
- יש להוסיף את התלות בכיתות.
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'
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
ומאפשר לך לבצע פעולות אספרסו ב-RecyclerView.
- צריך לוודא שמתבצעת קריאה ל-
navigate
עם הארגומנט הנכון.
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
השיטה verify
של Mockito' היא מה שמבדיל את המהלך הזה – אתם יכולים לאשר את ההדמיה של 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 כדי ליצור דוגמה של
NavController
. - יש לצרף את הקטע המבוזר
NavController
לקטע המקטע. - מוודאים שהניווט נקרא בפעולה ובפרמטרים הנכונים.
שלב 3. אופציונלי: כתיבה של ClickAddTaskbutton_navgateToAddEditFragment
כדי לבדוק אם אתם יכולים לכתוב בדיקת ניווט בעצמכם, נסו את המשימה הזו.
- כותבים את הבדיקה
clickAddTaskButton_navigateToAddEditFragment
: אם לוחצים על הסימן FAB, עוברים אל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)
)
)
}
אפשר ללחוץ כאן כדי לראות הבדל בין הקוד שהתחלת לבין הקוד הסופי.
כדי להוריד את הקוד עבור מעבדת הקוד הסופית, ניתן להשתמש בפקודת Git:
$ git clone https://github.com/googlecodelabs/android-testing.git $ cd android-testing $ git checkout end_codelab_2
לחלופין, אפשר להוריד את המאגר כקובץ ZIP, לפתוח אותו ב-Android Studio ולפתוח אותו.
בשיעור Lab הזה מתואר איך להגדיר הזרקת תלות ידנית, מאתר שירותים, ואיך להשתמש בזיוף ובדמיות באפליקציות של Kotlin ל-Android. בפרט:
- מה שרוצים לבדוק ושיטת הבדיקה קובעים את סוגי הבדיקות שרוצים להטמיע באפליקציה. בדיקות יחידה מתמקדות במהירות. בדיקות אינטגרציה מאמתות את האינטראקציה בין החלקים של התוכנית. בדיקות מקצה לקצה: התכונות מאומתות, שהאיכות שלהן גבוהה, השימוש בהן לעיתים קרובות עשוי להימשך זמן רב יותר.
- ארכיטקטורת האפליקציה משפיעה על הקושי לבדוק אותה.
- TDD או פיתוח מבוסס בדיקות הוא אסטרטגיה שבה כותבים תחילה את הבדיקות, ואז יוצרים את התכונה כדי לעבור את הבדיקות.
- כדי לבודד חלקים מהאפליקציה שלך לצורך בדיקה, אפשר להשתמש בכפולות בדיקה. כפולה של בדיקות היא גרסה של כיתה שנוצרה במיוחד לבדיקה. לדוגמה, אתם מזויפים את הנתונים ממסד נתונים או מהאינטרנט.
- אפשר להשתמש בהחדרת תלות כדי להחליף כיתה אמיתית בכיתת בדיקה, לדוגמה, מאגר או שכבת רשת.
- משתמשים בבדיקה יזומה (
androidTest
) כדי להפעיל רכיבי ממשק משתמש. - כאשר אין אפשרות להשתמש בהזרקת תלות של קבלן, לדוגמה, כדי להפעיל שבר, ניתן להשתמש בדרך כלל בממקם שירות. תבנית איתור השירות היא חלופה להזרקת תלות. היא כרוכה ביצירת סיווג יחיד הנקרא "Service Locator" ומטרתו היא לספק תלות, הן עבור הקוד הרגיל והן עבור קוד הבדיקה.
קורס אוניברסיטה:
התיעוד של מפתח Android:
- מדריך לארכיטקטורת אפליקציות
runBlocking
וכןrunBlockingTest
FragmentScenario
- אספרסו
- מוקיטו
- JUNIT4
- ספריית הבדיקה של AndroidX
- ספריית הליבה של רכיבי הארכיטקטורה של AndroidX
- קבוצות מקור
- בדיקה משורת הפקודה
סרטי וידאו:
אחר:
קישורים למעבדות אחרות של הקוד בקורס הזה זמינים בדף הנחיתה של מכשירי Android מתקדמים בדף Kotlin codelabs.