מבוא לבדיקת זוגות והזרקת תלות

מעבדת קוד זו היא חלק מהקורס המתקדם של Android בקורס Kotlin. כדי להפיק את המקסימום מהקורס הזה, אם עובדים על מעבדות הקוד ברצף, לא חובה לעשות זאת. כל שיעורי הקוד של הקורסים מופיעים בדף הנחיתה המתקדם של Android ב-Kotlin codelabs.

מבוא

מעבדת הקוד השנייה הזו עוסקת בכפולות בדיקה: מתי להשתמש בהן ב-Android ואיך להטמיע אותן באמצעות הזרקת תלות, דוגמת השירות והספריות. כך תלמדו איך לכתוב:

  • בדיקות של יחידות מאגר
  • מקטעי בדיקה ומודלים של תצוגות מפורטות
  • מקטעי ניווט מובנים

דברים שחשוב לדעת

כדאי שתכירו את:

מה תלמדו

  • איך לתכנן אסטרטגיית בדיקה
  • איך ליצור ולהשתמש בכפולות בדיקה, כלומר זיוף והדמיה
  • איך להשתמש בהזרקת תלות ידנית ב-Android לבדיקות של יחידות ושילוב
  • איך מחילים את הדפוס של ממקם השירות
  • איך לבדוק מאגרים, מקטעים, מקטעים של תצוגה ומודלים של ניווט

תשתמשו בספריות ובקונספטים הבאים של הקוד:

הפעולות שתבצעו:

  • כותבים בדיקות יחידה למאגר באמצעות הזרקה כפולה של הזרקה ותלויות.
  • כותבים בדיקות יחידה למודל תצוגה באמצעות הזרקת בדיקות כפולה והזרקת תלות.
  • כתיבת בדיקות שילוב עבור מקטעים ומודלים של תצוגה באמצעות מסגרת בדיקת ממשק משתמש Espresso.
  • כתיבת בדיקות ניווט באמצעות Mockito ו-Espresso.

בסדרה של משימות Lab אלה, אתם תעבדו עם אפליקציית ההערות לביצוע. האפליקציה מאפשרת לכתוב משימות כדי להשלים אותן ולהציג אותן ברשימה. תוכלו לסמן אותם כ'בוצעו' או 'לא בוצעו', לסנן אותם או למחוק אותם.

האפליקציה הזו כתובה ב-Kotlin, היא כוללת מספר מסכים, משתמשת ברכיבי Jetpack ועוקבת אחר הארכיטקטורה מתוך מדריך לארכיטקטורת אפליקציות. אם תבדקו איך לבדוק את האפליקציה הזו, תוכלו לבדוק אפליקציות שמשתמשות באותן ספריות ובארכיטקטורה.

להורדת הקוד

כדי להתחיל, יש להוריד את הקוד:

להורדת קובץ Zip

לחלופין, אפשר לשכפל את מאגר 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, מאגר וחדר. אם אתם מכירים את אחת מהדוגמאות הבאות, לאפליקציה הזו יש ארכיטקטורה דומה:

חשוב יותר להבין את הארכיטקטורה הכללית של האפליקציה מאשר להבין לעומק את הלוגיקה בכל שכבה.

זהו סיכום החבילות שתמצא:

חבילה: com.example.android.architecture.blueprints.todoapp

.addedittask

הוספה או עריכה של מסך משימה: קוד שכבה של ממשק משתמש להוספה או לעריכה של משימה.

.data

שכבת הנתונים: פעולה זו מתאימה לשכבת הנתונים של המשימות. הם כוללים את מסד הנתונים, הרשת וקוד המאגר.

.statistics

מסך הנתונים הסטטיסטיים: קוד שכבה של ממשק משתמש למסך הנתונים הסטטיסטיים.

.taskdetail

מסך פרטי המשימה: קוד שכבה של ממשק משתמש למשימה אחת.

.tasks

מסך המשימות: קוד שכבת ממשק המשתמש עבור הרשימה של כל המשימות.

.util

שיעורי עזר: כיתות משותפות המשמשות בחלקים שונים של האפליקציה. למשל, פריסת ההחלקה המשמשת במספר מסכים.

שכבת נתונים (.data)

אפליקציה זו כוללת סימולציה של שכבת רשת, בחבילה שלט רחוק, ושכבת מסד נתונים בחבילה מקומית. כדי לפשט את הדברים, בפרויקט הזה מתבצעת סימולציה של שכבת הרשת עם HashMap בלבד, עם עיכוב במקום ביצוע בקשות אמיתיות ברשת.

הקואורדינטות של DefaultTasksRepository מתווכות או מתווכות בין שכבת הרשת לבין שכבת מסד הנתונים, והיא זו שמחזירה נתונים לשכבת ממשק המשתמש.

שכבת ממשק משתמש ( .addedittask, .statistics, .taskdetail, .tasks)

כל אחת מהחבילות של שכבות ממשק המשתמש מכילה מקטע ומודל תצוגה, וכן כיתות אחרות הנדרשות עבור ממשק המשתמש (כגון מתאם עבור רשימת המשימות). הTaskActivity הוא הפעילות שמכילה את כל הקטעים.

ניווט

הניווט באפליקציה נשלט על ידי רכיב הניווט. הוא מוגדר בקובץ nav_graph.xml. הניווט מופעל במודלים של תצוגות מפורטות באמצעות הכיתה Event; המודלים של התצוגה קובעים גם אילו ארגומנטים יעברו. המקטעים מעיינים ב-Event ומבצעים את הניווט בפועל בין מסכים.

במעבדת קוד זו תלמדו איך לבדוק מאגרים, להציג מודלים ומקטעים באמצעות עותקים כפולים של בדיקות והזרקת תלות. לפני שמתחילים לחקור לעומק את הנושאים האלה, חשוב להבין את הסיבה שתנחה אתכם ואת הכתיבה של הבדיקות האלה.

הקטע הזה מפרט כמה שיטות מומלצות לבדיקה באופן כללי, כפי שהן רלוונטיות ל-Android.

פירמידת הבדיקה

כשחושבים על שיטת בדיקה, יש שלושה היבטים הקשורים לבדיקה:

  • היקף – באיזה חלק של הקוד הבדיקה מתייחסת? הבדיקות יכולות לפעול בשיטה אחת, באפליקציה כולה או במקום כלשהו ביניהן.
  • מהירות – באיזו מהירות הבדיקה מופעלת? מהירויות הבדיקה יכולות להשתנות מאלפיות שנייה לכמה דקות.
  • אמינות – איך "המציאות לדוגמה, אם חלק מהקוד שצריך לבדוק הוא לבצע בקשת רשת, האם קוד הבדיקה הוא למעשה שולח את בקשת הרשת הזו, או שהוא מזויף את התוצאה? אם בפועל הבדיקה היא ברשת, המשמעות היא שהאיכות שלה גבוהה יותר. החיסרון הוא שהבדיקה עשויה להימשך זמן רב יותר, והיא עלולה לגרום לשגיאות אם הרשת מושבתת או שהשימוש בה עלול להיות יקר.

קיימים פשרות מהותיות בין ההיבטים האלה. לדוגמה, המהירות והאמינות הן האיזון הנכון – בדרך כלל הבדיקה מהירה יותר, ולהיפך. אחת מהדרכים הנפוצות ביותר לחלק בדיקות אוטומטיות היא לשלוש הקטגוריות האלה:

  • בדיקות יחידה – בדיקות ממוקדות מאוד שפועלות בכיתה אחת, בדרך כלל באותה שיטה. אם בדיקת יחידה תיכשל, תוכלו לדעת בדיוק היכן נמצאת הבעיה בקוד. האיכות שלהן נמוכה מאוד, מאחר שבעולם האמיתי, האפליקציה שלכם כוללת הרבה יותר מביצוע של שיטה אחת או מחלקה אחת. הם מהירים מספיק כדי לפעול בכל פעם שאתה משנה את הקוד. לרוב, הן יעברו בדיקות מקומיות (בקבוצת המקור של test). דוגמה: בדיקת שיטות יחיד במודלים ובמאגרי תצוגות.
  • בדיקות שילוב — הבדיקות האלה מאפשרות לבחון את האינטראקציה של כמה כיתות כדי לוודא שהן פועלות כמצופה כשמשתמשים בהן יחד. אחת הדרכים לבנות בדיקות שילוב היא לבקש שהן יבדקו תכונה אחת, כמו היכולת לשמור משימה. הם בודקים טווח רחב יותר של קוד מאשר בדיקות יחידה, אבל הם עדיין מותאמים להפעלה מהירה, לעומת אמינות מלאה. ניתן להריץ אותן באופן מקומי או כבדיקות אינסטרומנטציה, בהתאם למצב. דוגמה: בדיקה של כל הפונקציונליות של שבר יחיד ושל זוג מודלים של תצוגה.
  • בדיקות מקצה לקצה (E2e) – כדאי לבדוק שילוב של תכונות שפועלות יחד. הם בודקים חלקים גדולים של האפליקציה, מדמים שימוש אמיתי מקרוב, ולכן בדרך כלל הם איטיים. רמת המהימנות הגבוהה ביותר היא שמספר האפליקציות שלך אכן פועל במתכונת כוללת. בגדול, הבדיקות האלה יהיו בדיקות אינסטרומנטטיביות (בקבוצת המקור של androidTest)
    דוגמה: הפעלה של האפליקציה כולה ובדיקה של כמה תכונות יחד.

לרוב, שיעור ההצעות של הבדיקות האלה מיוצג על ידי פירמידה, ורוב הבדיקות הן יחידות יחידה.

ארכיטקטורה ובדיקה

היכולת שלך לבדוק את האפליקציה בכל הרמות השונות של פירמידת הבדיקה קשורה באופן מהותי לארכיטקטורה של האפליקציה. לדוגמה, אפליקציה שקשה מאוד להשתמש בה, עם מבנה אדריכלי שגוי עלולה לכלול את כל הלוגיקה שלה בשיטה אחת. ייתכן שתוכלו לכתוב בדיקה מקצה לקצה עבור הבדיקה הזו, כי ברוב המקרים הבדיקות האלה בודקות חלקים גדולים מהאפליקציה, אבל מה לגבי כתיבת יחידות ניסוי או שילוב? כשכל הקוד מרוכז במקום אחד, קשה לבדוק רק את הקוד שקשור ליחידה או לתכונה אחת.

גישה טובה יותר היא לחלק את הלוגיקה של האפליקציה למספר שיטות וכיתות, כך שכל בדיקה תבודד. ארכיטקטורה היא דרך לחלק את הקוד ולארגן אותו, וכך קל יותר לבדוק יחידות ושילובים. אפליקציית TO-Do שעליך לבדוק תואמת לארכיטקטורה מסוימת:



בשיעור זה תלמדו איך לבדוק חלקים מהארכיטקטורה שלמעלה בנפרד, בנפרד

  1. תחילה עליכם לבדוק את היחידה של המאגר.
  2. לאחר מכן תשתמשו בכפולה בדיקה של מודל המודלים של התצוגה המפורטת, הנדרשת לבדיקת יחידה ולבדיקת שילוב של מודל התצוגה המפורטת.
  3. בשלב הבא, תלמדו איך לכתוב בדיקות שילוב עבור מקטעים ומודלים של התצוגה המפורטת שלהם.
  4. לבסוף, תלמדו איך לכתוב בדיקות שילוב שכוללות את רכיב הניווט.

בדיקת קצה-קצה תכסה בשיעור הבא.

כשכותבים בדיקת יחידה לחלק מכיתה (שיטה או אוסף קטן של שיטות), המטרה היא לבדוק רק את הקוד של הכיתה.

לא מספיק לבדוק רק קוד בכיתה או בכיתות מסוימים. נבחן את הדוגמה הבאה. פותחים את הכיתה 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&QUOTE;

הדמיה

כפולה ניסיונית שעוקבת אילו מהשיטות שלה נקראו. לאחר מכן היא עוברת את הבדיקה או כושלת, בהתאם לשיטת הקריאה שלה.

מקפצה

כפל בדיקה שאינו כולל לוגיקה ומחזיר רק את מה שאתם מתכננים להחזיר. לדוגמה, אפשר לתכנת StubTaskRepository כדי להחזיר שילובים מסוימים של משימות מ-getTasks.

דמה

כפולה מבוטא שמועברת אך לא בשימוש, למשל, רק צריך לספק אותה כפרמטר. אם היה לך NoOpTaskRepository, הוא היה מטמיע את ה-TaskRepository בלי קוד no בכל אחת מהשיטות.

ריגול

כפולה של בדיקה, שגם עוקבת אחר מידע נוסף נוסף. לדוגמה, אם ביצעת SpyTaskRepository, ייתכן שיתבצע מעקב אחר מספר הפעמים ששיטה addTask נקראה.

מידע נוסף על הכפלות בבדיקה זמין במאמר בדיקות בשירותים: בדקו את כפולה שלך.

כפול המצבים הנפוצים ביותר בבדיקה ב-Android הם זיוף והדמיה.

במשימה הזו, בכוונתך ליצור בדיקה כפולה של FakeDataSource ליחידה אחת (DefaultTasksRepository) המופרדת ממקורות הנתונים בפועל.

שלב 1: יוצרים את הכיתה FakeDataSource

בשלב זה יוצרים כיתה שנקראת FakeDataSouce, והיא תהיה כפיל בבדיקה של LocalDataSource ו-RemoteDataSource.

  1. בקבוצת המקורות test (בדיקה), לוחצים לחיצה ימנית על New -> Package (חדש – חבילה).

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

שלב 2: מטמיעים את ממשק TasksDataSource

כדי שתהיה לך אפשרות להשתמש בכיתה החדשה שלך ב-FakeDataSource כמכפיל בדיקה, היא צריכה להחליף את מקורות הנתונים האחרים. מקורות הנתונים האלה הם TasksLocalDataSource ו-TasksRemoteDataSource.

  1. שימו לב איך שתיהן מטמיעות את ממשק TasksDataSource.
class TasksLocalDataSource internal constructor(
    private val tasksDao: TasksDao,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }

object TasksRemoteDataSource : TasksDataSource { ... }
  1. הגדרת FakeDataSource להטמעה של TasksDataSource:
class FakeDataSource : TasksDataSource {

}

תלונות לגבי Android Studio לא הטמעת את השיטות הנדרשות עבור TasksDataSource.

  1. משתמשים בתפריט התיקון המהיר ובוחרים באפשרות הטמעת חברים.


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

שלב 3: הטמעת שיטת getTasks ב-FakeDataSource

FakeDataSource הוא סוג ספציפי של מכפיל בדיקה שנקרא זיוף. מזויף הוא העתק כפול לבדיקה, שההטמעה שלו כוללת גם את יישום הכיתוב אבל הוא מיושם באופן שמתאים לבדיקות, אך אינו מתאים לייצור. "Working" המשמעות היא שהכיתה תיצור פלט מציאותי בהינתן קלט.

לדוגמה, מקור הנתונים המזויף לא יתחבר לרשת ולא ישמור שום דבר במסד נתונים, אלא רק ישתמש ברשימת זיכרון. הפעולה הזו&תפעל; כפי שציפיתם; &בשיטות האלה, כדי לקבל או לשמור משימות יוחזרו תוצאות צפויות, אך אף פעם לא תוכלו להשתמש בהטמעה הזו בסביבת ייצור כי היא לא נשמרת בשרת או במסד נתונים.

תמריץ FakeDataSource

  • מאפשר לך לבדוק את הקוד ב-DefaultTasksRepository מבלי להסתמך על מסד נתונים או רשת אמיתיים.
  • מספק "real-eallow" לצורך בדיקות.
  1. יש לשנות את המבנה של FakeDataSource כדי ליצור var בשם tasks שהוא MutableList<Task>? עם ערך ברירת מחדל של רשימה ריקה.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }


זו רשימת המשימות ש&מירכאות, מירכאות ופירושן מסד נתונים או תגובה של שרת. בשלב הזה, המטרה היא לבדוק את השיטה של המאגר getTasks. כך ניתן להשתמש בשיטות מקור הנתונים& getTasks, deleteAllTasks ו-saveTask.

יש לכתוב גרסה מזויפת של השיטות הבאות:

  1. כותבים getTasks: אם tasks לא null, מחזירים תוצאה Success. אם הערך של tasks הוא null, יש להחזיר תוצאה Error.
  2. כתיבת deleteAllTasks: מחיקת רשימת המשימות המשתנות.
  3. כתיבה 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

  1. יש לשנות את ה-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 }
  1. מאחר שעברת את התלות ב-, יש להסיר את השיטה init. אין יותר צורך ליצור תלויות.
  2. למחוק גם את משתני המופע הישנים. אתה מגדיר אותם בבונה:

DefaultTasksRepository.kt

// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
  1. לבסוף, יש לעדכן את השיטה 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.

  1. לוחצים לחיצה ימנית על שם הכיתה DefaultTasksRepository ובוחרים באפשרות יצירה, ולאחר מכן באפשרות בדיקה.
  2. מבצעים את ההוראות ליצירת DefaultTasksRepositoryTest בקבוצת המקור בדיקה.
  3. בחלק העליון של סיווג 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 }
  1. יוצרים שלושה משתנים, שני משתנים של 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.

  1. יש ליצור שיטה בשם createRepository ולהוסיף לה הערות באמצעות @Before.
  2. יצירת מקורות נתונים מזויפים באמצעות רשימות remoteTasks ו-localTasks.
  3. תוכלו ליצור מצב מיידי באמצעות 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.

  1. כתיבת בדיקה לשיטה 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 ואתם צריכים להפעיל שגרת נתונים כדי להפעיל אותה. לשם כך, תצטרכו היקף ברמת קורטה. כדי לפתור את השגיאה הזו, תצטרכו להוסיף משתנים תלויים לטיפול בהפעלת שגרת שגרה בבדיקות.

  1. על ידי שימוש ב-testImplementation אפשר להוסיף את הגורמים התלויים הדרושים לבדיקה של קוראונים.

app/build.gradle

testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

לא לשכוח לסנכרן!

kotlinx-coroutines-test היא ספריית בדיקות הקורינתן, שמיועדת במיוחד לבדיקת קוראוטן. כדי להריץ את הבדיקות, צריך להשתמש בפונקציה runBlockingTest. פונקציה זו מסופקת על ידי ספריית הבדיקה של coroutines. היא מקבלת קטע קוד ולאחר מכן מפעילה את בלוק הקוד הזה בהקשר יוצא דופן של קורונה, שפועל באופן סינכרוני ומיידי. כלומר, פעולות יתרחשו בסדר יורד. באופן כללי, המטרה היא שהקורינות שלך יפעלו כמו שגרתיים, ולכן היא מיועדת לבדיקת קוד.

צריך להשתמש בפונקציה runBlockingTest בכיתות הבדיקה כשמתקשרים לפונקציה suspend. בהסבר נוסף על אופן הפעולה של runBlockingTest ואיך לבדוק שגרות כאלה במעבדת הקוד הבאה בסדרה.

  1. מוסיפים את הסמל @ExperimentalCoroutinesApi מעל הכיתה. הפעולה הזו מבטאת את אישורך לכך שייעשה שימוש ב-API של שגרת המשך (runBlockingTest) בכיתה. בלעדיו, תופיע לך אזהרה.
  2. בחזרה ב-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))
    }

}
  1. כדאי להריץ את הבדיקה החדשה ל-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.

  1. פותחים את DefaultTasksRepository ולוחצים לחיצה ימנית על שם הכיתה. לאחר מכן בוחרים באפשרות גורם חדש -> חילוץ -> ממשק.

  1. בוחרים באפשרות חילוץ כדי להפריד בין קבצים.

  1. בחלון API של חילוץ משנים את שם הממשק ל-TasksRepository.
  2. בקטע ממשק ליצירת חברים, מסמנים את כל החברים מלבד שני החברים הנלווים והשיטות הפרטיות.


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

כמו כן, DefaultTasksRepository מטמיע עכשיו את TasksRepository.

  1. מפעילים את האפליקציה (לא את הבדיקות) כדי לוודא שהכול פועל כמו שצריך.

שלב 2. יצירת FakeTestRepository

עכשיו יש לך ממשק, ואפשר ליצור כפול של הבדיקה. DefaultTaskRepository

  1. במקור הבדיקה, בנתונים/מקור יוצרים את קובץ Kotlin ומחלקה FakeTestRepository.kt ומרחיבים אותו מהממשק של TasksRepository.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository  {
}

תקבלו הנחיות להטמיע את שיטות הממשק.

  1. מעבירים את העכבר מעל השגיאה עד שרואים את תפריט ההצעות, ואז לוחצים על צירוף משתמשים ובוחרים בהם.
  1. בוחרים את כל השיטות ולוחצים על אישור.

שלב 3. הטמעת שיטות של FakeTestRepository

יש לך עכשיו כיתה FakeTestRepository עם שיטות &ציטוט; לא מיושמות. בדומה לאופן שבו הטמעת את FakeDataSource, FakeTestRepository יגובה על ידי מבנה נתונים במקום להתמודד עם תהליך בחירת רשת מורכב בין מקורות נתונים מקומיים לבין מקורות מרוחקים.

לתשומת ליבך, אין ב-FakeTestRepository צורך להשתמש ב-FakeDataSource או בכלים כאלה. פשוט צריך להחזיר פלט מזויף מציאותי נתון. קובץ LinkedHashMap ישמש לאחסון של רשימת המשימות ולרשימת המשימות השמורים שלך, שניתן להשתמש בה ב-MutableLiveData.

  1. בתיקייה FakeTestRepository, מוסיפים גם משתנה LinkedHashMap שמייצג את רשימת המשימות הנוכחית וגם MutableLiveData בשביל המשימות שניתנות למדידה.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private val observableTasks = MutableLiveData<Result<List<Task>>>()


    // Rest of class
}

ליישם את השיטות הבאות:

  1. getTasks – שיטה זו צריכה להפוך את tasksServiceData ולהפוך אותה לרשימה באמצעות tasksServiceData.values.toList(). לאחר מכן יש להחזיר את התוצאה כתוצאה מ-Success.
  2. refreshTasks – מתבצע עדכון של הערך observableTasks לערך המוחזר על ידי getTasks().
  3. 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 כמה פעמים, אבל כדי להקל על התהליך, יש להוסיף שיטת עזרה ספציפית לבדיקות שמאפשרות לך להוסיף משימות.

  1. יש להוסיף את שיטת addTasks, שמתבצעת ב-vararg משימות, מוסיפה כל אחת ל-HashMap ולאחר מכן לרענן את המשימות.

FakeTestRepository.kt

    fun addTasks(vararg tasks: Task) {
        for (task in tasks) {
            tasksServiceData[task.id] = task
        }
        runBlocking { refreshTasks() }
    }

בשלב זה יש לכם מאגר מזויף לבדיקות עם כמה שיטות מרכזיות שמוטמעות. השלב הבא, הוא להשתמש בבדיקה שלך!

במשימה הזו עליך להשתמש בכיתה מזויפת מתוך ViewModel. אפשר להשתמש בהזרקת תלות של קבלן כדי לקבל את שני מקורות הנתונים דרך הזרקת תלות של קבלן על ידי הוספת משתנה TasksRepository לבנייה של TasksViewModel&#39.

התהליך הזה שונה מעט עם מודלים של תצוגות מפורטות מפני שאתם לא יוצרים אותם ישירות. למשל:

class TasksFragment : Fragment() {

    private val viewModel by viewModels<TasksViewModel>()
    
    // Rest of class...

}


כמו בקוד שלמעלה, אתם משתמשים בהענקת גישה לנכס של viewModel's, שיוצר את מודל התצוגה המפורטת. כדי לשנות את אופן היצירה של מודל התצוגה, צריך להוסיף ViewModelProvider.Factory ולהשתמש בו. אם אינכם מכירים את ViewModelProvider.Factory, תוכלו לקבל מידע נוסף בנושא כאן.

שלב 1. יצירה של מפעל ViewView ב-TasksViewModel ושימוש בו

אפשר להתחיל לעדכן את הכיתות ולבדוק את מה שקשור למסך Tasks.

  1. פתוח TasksViewModel.
  2. צריך לשנות את המבנה של 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, אבל אפשר גם לרשום אותו בקובץ נפרד.

  1. בחלק התחתון של הקובץ 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. עכשיו, אחרי שיצרתם את המפעל, תוכלו להשתמש בו בכל מקום שבו אתם בונים את מודל התצוגה המפורטת.

  1. יש לעדכן את TasksFragment כדי להשתמש במפעל.

TasksFragment.kt

// REPLACE
private val viewModel by viewModels<TasksViewModel>()

// WITH

private val viewModel by viewModels<TasksViewModel> {
    TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. צריך להריץ את קוד האפליקציה ולוודא שהכול עדיין תקין.

שלב 2. שימוש ב-FakeTestRepository בתוך TasksViewModelTest

עכשיו, במקום להשתמש במאגר האמיתי בבדיקות של מודל התצוגה המפורטת, אפשר להשתמש במאגר מזויף.

  1. פותחים את TasksViewModelTest.
  2. הוספת נכס 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
}
  1. יש לעדכן את השיטה 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)
        
    }
  1. מכיוון שהפסקת להשתמש בקוד AndroidX Testing ApplicationProvider.getApplicationContext, אפשר גם להסיר את ההערה @RunWith(AndroidJUnit4::class).
  2. כדאי להריץ את הבדיקות כדי לוודא שהן עדיין פועלות!

על ידי הזרקה של תלות בבונה, הסרת את הDefaultTasksRepository כתלויה והחלפת אותו ב-FakeTestRepository בבדיקות.

שלב 3. עדכון גם מקטע TaskDetail ו-ViewModel

יש לבצע את אותם שינויים ב-TaskDetailFragment וב-TaskDetailViewModel. הפעולה הזו תכין את הקוד לקריאה הבאה של TaskDetail בדיקות.

  1. פתוח TaskDetailViewModel.
  2. עדכן את ה-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 }
  1. בחלק התחתון של הקובץ 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)
}
  1. יש לעדכן את TasksFragment כדי להשתמש במפעל.

TasksFragment.kt

// REPLACE
private val viewModel by viewModels<TaskDetailViewModel>()

// WITH

private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. מפעילים את הקוד ומוודאים שהכול פועל כמו שצריך.

עכשיו ניתן להשתמש ב-FakeTestRepository במקום במאגר האמיתי ב-TasksFragment וב-TasksDetailFragment.

בשלב הבא, יש לכתוב בדיקות שילוב כדי לבדוק את האינטראקציות בין חלקי הקוד והמודל. תוכלו לבדוק אם קוד הדגם שלכם מתעדכן כראוי בממשק המשתמש. כדי לעשות את זה

  • תבנית ה-ServiceLocator
  • ספריות אספרסו ומוקיטו

בדיקות שילוב בודקים את האינטראקציה בין כמה כיתות כדי לוודא שהן פועלות כמצופה כשמשתמשים בהן יחד. אפשר להריץ את הבדיקות האלה באופן מקומי (קבוצת מקורות אחת (test) או כבדיקות אינסטרומנטציה (קבוצת מקורות מסוג androidTest).

במקרה שלך עליך לבצע בדיקה של כל מקטע ולכתוב בדיקות שילוב של החלק והתצוגה כדי לבדוק את התכונות העיקריות של המקטע.

שלב 1. הוספת יחסי תלות של שולי האזור

  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 — ספריית בדיקות ליבה של AndroidX
  • kotlinx-coroutines-test – ספריית בדיקות הקורינת
  • androidx.fragment:fragment-testing — ספריית בדיקות AndroidX ליצירת מקטעים בבדיקות ושינוי המצב שלהם.

מאחר שהספריות האלה יהיו בשימוש בקבוצת המקור שלך ב-androidTest, עליך להשתמש ב-androidTestImplementation כדי להוסיף אותן כתלויות.

שלב 2. יצירת סיווג של TaskDetailFragmentTest

בTaskDetailFragment מוצג מידע על משימה אחת.

אפשר להתחיל בדיקת קטע של TaskDetailFragment כי יש לו פונקציונליות בסיסית למדי בהשוואה למקטעים אחרים.

  1. פתוח taskdetail.TaskDetailFragment.
  2. יוצרים בדיקה עבור TaskDetailFragment, כפי שעשיתם בעבר. עליך לבחור את אפשרויות ברירת המחדל ולהציב אותן בקבוצת המקורות androidTest (לא בקבוצת המקור test).

  1. מוסיפים את ההערות הבאות לכיתה TaskDetailFragmentTest.

TaskDetailFragmentTest.kt

@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {

}

מטרת ההערה:

  • @MediumTest – סימון הבדיקה כ"זמן ריצה וזמן ריצה;;; בדיקת שילוב כך תוכלו לקבץ ולבחור את גודל הבדיקה להרצה.
  • @RunWith(AndroidJUnit4::class)—משמש בכל כיתה באמצעות בדיקת AndroidX.

שלב 3. הפעלת קטע קוד מבדיקה

במשימה הזו, בכוונתך להפעיל את TaskDetailFragment באמצעות ספריית הבדיקה של AndroidX. FragmentScenario היא כיתה מבדיקת AndroidX המקיפה שבר ומעניקה לכם שליטה ישירה במחזור החיים של המקטע. כדי לכתוב בדיקות עבור מקטעים, צריך ליצור FragmentScenario לשבר שאתם בודקים (TaskDetailFragment).

  1. אפשר להעתיק את הבדיקה הזו ל-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 עם החבילה והעיצוב האלה.

זו עדיין לא בדיקה גמורה, כי היא לא מהצהירה על שום דבר. בינתיים, אפשר להריץ את הבדיקה ולבדוק מה קורה.

  1. זוהי בדיקה אינסטרומנטמית, ולכן יש לוודא שהאמולטור או המכשיר שלך גלויים.
  2. מריצים את הבדיקה.

יש כמה דברים שיכולים לקרות.

  • ראשית, מאחר שמדובר בבדיקה אינסטרומנטלית, הבדיקה תתבצע במכשיר הפיזי שלך (אם יש חיבור) או אמולטור.
  • הוא אמור להפעיל את המקטע.
  • שימו לב איך הוא לא מנווט בקטע אחר כלשהו או שיש בו תפריטים המשויכים לפעילות – הוא רק החלק.

לבסוף, מעיינים היטב ומציינים שקטע הקוד מציין "אין נתונים" מאחר שהוא לא טוען בהצלחה את נתוני המשימה.

בבדיקה צריך לטעון את 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, יש לבצע את הפעולות הבאות:

  1. יצירה של סיווג ממקם שירות שיכול לבנות ולאחסן מאגר. כברירת מחדל, הוא יוצר מאגר "regular" .
  2. מגדירים מחדש את הקוד כך שכאשר יהיה צורך במאגר, השתמשו בממקם השירות.
  3. בשיעור הבדיקה, קוראים לשיטה בממקם השירות שמחליפה את מאגר הכתובות "normal" בכפולה של הבדיקה.

שלב 1. יצירה של ServiceLocator

קדימה, מתחילים כיתה של ServiceLocator. הוא ישותף בקוד המקור שהוגדר יחד עם שאר קוד האפליקציה, מכיוון שהוא משמש בקוד האפליקציה הראשי.

הערה: ServiceLocator היא יחידה, לכן צריך להשתמש במילת המפתח Kotlin object לכיתה.

  1. יוצרים את הקובץ ServiceLocator.kt ברמה העליונה של קבוצת המקור הראשית.
  2. הגדרה של object בשם ServiceLocator.
  3. יש ליצור משתני מופע database ו-repository ולהגדיר את שניהם כ-null.
  4. מוסיפים הערה למאגר באמצעות @Volatile כי ניתן להשתמש בו במספר שרשורים (@Volatile מוסבר בפירוט כאן).

הקוד אמור להיראות כמו שמוצג למטה.

object ServiceLocator {

    private var database: ToDoDatabase? = null
    @Volatile
    var tasksRepository: TasksRepository? = null

}

כרגע, הדבר היחיד שServiceLocator צריך לעשות הוא לדעת איך להחזיר TasksRepository. הפונקציה תחזיר DefaultTasksRepository קיים או תיצור DefaultTasksRepository חדש ותחזיר אותו לפי הצורך.

מגדירים את הפונקציות הבאות:

  1. provideTasksRepository—אפשר להוסיף מאגר קיים או ליצור מאגר חדש. השיטה הזו אמורה להיות synchronized בתאריך this כדי למנוע, במקרים שבהם פועלים כמה שרשורים, בטעות כדי ליצור שני מופעים של מאגר נתונים.
  2. createTasksRepository—קוד ליצירת מאגר חדש. התקשרות אל createTaskLocalDataSource ויצירת TasksRemoteDataSource חדש.
  3. createTaskLocalDataSource– קוד ליצירת מקור נתונים מקומי חדש. התקשרות אל createDataBase.
  4. 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.

חשוב ליצור רק מופע אחד של סיווג מאגר הנתונים. כדי לוודא זאת, עליך להשתמש בממקם השירות בכיתה שלי.

  1. ברמה העליונה של היררכיית החבילות, פותחים את 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.

  1. פותחים את 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.

  1. יש לפתוח את TaskDetailFragement ולמצוא את השיחה עם getRepository בחלק העליון של הכיתה.
  2. אפשר להחליף את השיחה הזו בשיחה שמקבלת את המאגר מ-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)
}
  1. חוזרים על הפעולה עבור 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)
    }
  1. ב-StatisticsViewModel וב-AddEditTaskViewModel, יש לעדכן את הקוד שמקבל את המאגר כדי להשתמש במאגר מ-TodoApplication.

TasksFragment.kt

// REPLACE this code
    private val tasksRepository = DefaultTasksRepository.getRepository(application)



// WITH this code

    private val tasksRepository = (application as TodoApplication).taskRepository

  1. מריצים את האפליקציה (ולא את הבדיקה)!

מאחר שהוספנו מחדש את הפרמטר רק לאפליקציה, האפליקציה צריכה לפעול באותו אופן ללא בעיה.

שלב 3. יצירת FakeAndroidTestRepository

FakeTestRepository כבר הוגדר בקבוצת מקורות הבדיקה. כברירת מחדל, לא ניתן לשתף כיתות בדיקה בין קבוצת המקור test לבין androidTest קבוצות של מקורות. לכן, עליך ליצור כפילות אחת (FakeTestRepository) בקבוצת המקור androidTest, ולקרוא לה FakeAndroidTestRepository.

  1. לוחצים לחיצה ימנית על קבוצת המקור של androidTest ויוצרים חבילת נתונים. לוחצים לחיצה ימנית שוב ויוצרים חבילת מקור.
  2. יצירת מחלקה חדשה בחבילת המקור הזו בשם FakeAndroidTestRepository.kt.
  3. מעתיקים את הקוד הבא לכיתה.

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.

  1. פתוח ServiceLocator.kt.
  2. סימון המגדיר של tasksRepository בתור @VisibleForTesting. הערה זו מאפשרת לבטא את הסיבה שבגללה מגדירים את הציבור היא בגלל בדיקה.

ServiceLocator.kt

    @Volatile
    var tasksRepository: TasksRepository? = null
        @VisibleForTesting set

גם אם אתם מריצים את הבדיקה בלבד וגם אם מדובר בקבוצת בדיקות, הבדיקות צריכות להיות זהות. המשמעות היא שהבדיקות לא צריכות להיות התנהגות שתלויות זו בזו (כלומר, הימנעות משיתוף אובייקטים בין בדיקות).

מאחר שה-ServiceLocator הוא יחידה, ניתן לשתף אותו בטעות בין בדיקות. כדי למנוע זאת, כדאי ליצור שיטה שתאפס בצורה נכונה את מצב ServiceLocator בין בדיקות.

  1. יש להוסיף משתנה מכונה בשם lock עם הערך Any.

ServiceLocator.kt

private val lock = Any()
  1. יש להוסיף שיטה ספציפית לבדיקה בשם 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.

  1. פתוח TaskDetailFragmentTest.
  2. הצהרה על משתנה lateinit TasksRepository.
  3. יש להוסיף הגדרה ופרק דרך כדי להגדיר FakeAndroidTestRepository לפני כל בדיקה ולנקות אותה אחרי כל בדיקה.

TaskDetailFragmentTest.kt

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }
  1. גוף הטקסט של activeTaskDetails_DisplayedInUi() ב-runBlockingTest.
  2. צריך לשמור את 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)

    }
  1. סימון הערות לכל הכיתה באמצעות @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)

    }

}
  1. מריצים את בדיקת 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. השבתת האנימציות

בדיקות אספרסו פועלות במכשיר אמיתי ולכן הן בדיקות אינסטרומנטציה שמבוצעות בטבע. אחת מהבעיות שצצות היא אנימציות: אם אנימציה נעצרת ואתם מנסים לבדוק אם תצוגה מסוימת מוצגת במסך, אבל היא עדיין מבצעת אנימציה, אספרסו עלול להיכשל בבדיקה. פעולה זו עלולה לגרום לבדיקות אספרסו רעועות.

בבדיקה של ממשק המשתמש של אספרסו, השיטה המומלצת היא לכבות את האנימציות (גם הבדיקה שלך תרוץ מהר יותר!):

  1. במכשיר הבדיקה, עוברים אל הגדרות > אפשרויות למפתחים.
  2. משביתים את שלוש ההגדרות האלה: קנה מידה של אנימציה של חלון, קנה מידה של אנימציה במעבר וטווח משך אנימציה.

שלב 3. בדיקה של אספרסו

לפני שכותבים בדיקת אספרסו, אפשר לעיין בקוד של אספרסו.

onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))

בהצהרה הזו מופיעה תצוגת תיבת הסימון עם המזהה task_detail_complete_checkbox, לוחצים עליה ואז היא מאשרת שהיא מסומנת.

רוב הצהרות אספרסו מורכבות מארבעה חלקים:

1. שיטת אספרסו סטטית

onView

onView היא דוגמה לשיטה של אספרסו סטטי שמתחילה בהצהרת אספרסו. onView היא אחת האפשרויות הנפוצות ביותר, אבל יש אפשרויות נוספות, כמו onData.

2. ViewMATCHer

withId(R.id.task_detail_title_text)

withId היא דוגמה של ViewMatcher שמקבלת צפייה לפי המזהה שלה. יש התאמות תצוגה נוספות שניתן למצוא בתיעוד.

3. ViewAction

perform(click())

שיטת perform שמשתמשת ב-ViewAction. ViewAction הוא פעולה שאפשר לבצע בתצוגה, לדוגמה, כאן היא לוחץ על התצוגה.

4. ViewAssertion

check(matches(isChecked()))

check שיש בו ViewAssertion. ViewAssertion בודקים או מצהירים על משהו בנוגע לתצוגה. ההצהרת ViewAssertion הנפוצה ביותר שבה תשתמשו היא הטענה matches. כדי לסיים את ההצהרה, יש להשתמש בViewMatcher אחר, במקרה הזה isChecked.

לתשומת ליבך, לא תמיד להתקשר אל perform וגם אל check בהצהרת אספרסו. אפשר להשתמש בהצהרות שמבצעות רק הצהרה באמצעות check או פשוט ViewAction כדי להשתמש ב-perform.

  1. פתוח TaskDetailFragmentTest.kt.
  2. מעדכנים את הבדיקה של 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
  1. כל התוכן שמופיע אחרי התגובה של // THEN כולל אספרסו. יש לבדוק את מבנה הבדיקה ואת השימוש ב-withId כדי לבדוק איך דף הפרטים אמור להיראות.
  2. מריצים את הבדיקה ומוודאים שהיא עברה.

שלב 4. אופציונלי: כותבים בדיקת אספרסו משלכם

עכשיו אתם יכולים לכתוב בדיקה בעצמכם.

  1. יצירת בדיקה חדשה בשם 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
}
  1. כדי לראות את הבדיקה הקודמת, יש להשלים את הבדיקה.
  2. להריץ ולאשר את מעברי הבדיקה.

ה-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. הוספת יחסי תלות של שולי האזור

  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

  1. פתיחת TasksFragment
  2. לוחצים לחיצה ימנית על שם הכיתה TasksFragment ובוחרים באפשרות יצירה ואז באפשרות בדיקה. יוצרים בדיקה בקבוצת המקור androidTest.
  3. העתקת הקוד הזה אל 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 הנכון.

  1. מוסיפים את הבדיקה 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)
        
    }
  1. כדי ליצור הדמיה, צריך להשתמש בפונקציה mock של Mockito'

TasksFragmentTest.kt

 val navController = mock(NavController::class.java)

כדי לדמות את Mockito, עליך להעביר את הכיתה שברצונך לדמות.

בשלב הבא, עליך לשייך את NavController לקטע הקוד. onFragment מאפשר להפעיל שיטות בשבר עצמו.

  1. עליך לנסח את הקטע החדש של המקטע NavController.
scenario.onFragment {
    Navigation.setViewNavController(it.view!!, navController)
}
  1. מוסיפים את הקוד כדי ללחוץ על הפריט ב-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.

  1. צריך לוודא שמתבצעת קריאה ל-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")
    )
}
  1. כדאי להריץ את הבדיקה!

בסיכום, כדי לבדוק את הניווט אפשר:

  1. יש להשתמש ב-Mockito כדי ליצור דוגמה של NavController.
  2. יש לצרף את הקטע המבוזר NavController לקטע המקטע.
  3. מוודאים שהניווט נקרא בפעולה ובפרמטרים הנכונים.

שלב 3. אופציונלי: כתיבה של ClickAddTaskbutton_navgateToAddEditFragment

כדי לבדוק אם אתם יכולים לכתוב בדיקת ניווט בעצמכם, נסו את המשימה הזו.

  1. כותבים את הבדיקה 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 ולפתוח אותו.

להורדת קובץ Zip

בשיעור Lab הזה מתואר איך להגדיר הזרקת תלות ידנית, מאתר שירותים, ואיך להשתמש בזיוף ובדמיות באפליקציות של Kotlin ל-Android. בפרט:

  • מה שרוצים לבדוק ושיטת הבדיקה קובעים את סוגי הבדיקות שרוצים להטמיע באפליקציה. בדיקות יחידה מתמקדות במהירות. בדיקות אינטגרציה מאמתות את האינטראקציה בין החלקים של התוכנית. בדיקות מקצה לקצה: התכונות מאומתות, שהאיכות שלהן גבוהה, השימוש בהן לעיתים קרובות עשוי להימשך זמן רב יותר.
  • ארכיטקטורת האפליקציה משפיעה על הקושי לבדוק אותה.
  • TDD או פיתוח מבוסס בדיקות הוא אסטרטגיה שבה כותבים תחילה את הבדיקות, ואז יוצרים את התכונה כדי לעבור את הבדיקות.
  • כדי לבודד חלקים מהאפליקציה שלך לצורך בדיקה, אפשר להשתמש בכפולות בדיקה. כפולה של בדיקות היא גרסה של כיתה שנוצרה במיוחד לבדיקה. לדוגמה, אתם מזויפים את הנתונים ממסד נתונים או מהאינטרנט.
  • אפשר להשתמש בהחדרת תלות כדי להחליף כיתה אמיתית בכיתת בדיקה, לדוגמה, מאגר או שכבת רשת.
  • משתמשים בבדיקה יזומה (androidTest) כדי להפעיל רכיבי ממשק משתמש.
  • כאשר אין אפשרות להשתמש בהזרקת תלות של קבלן, לדוגמה, כדי להפעיל שבר, ניתן להשתמש בדרך כלל בממקם שירות. תבנית איתור השירות היא חלופה להזרקת תלות. היא כרוכה ביצירת סיווג יחיד הנקרא "Service Locator" ומטרתו היא לספק תלות, הן עבור הקוד הרגיל והן עבור קוד הבדיקה.

קורס אוניברסיטה:

התיעוד של מפתח Android:

סרטי וידאו:

אחר:

קישורים למעבדות אחרות של הקוד בקורס הזה זמינים בדף הנחיתה של מכשירי Android מתקדמים בדף Kotlin codelabs.