בדיקות יחידה ל-Android: היכן ההיגיון נשבר ואיך לתפוס זאת
ההיגיון של אפליקציית Android נשבר לא בפריסה, אלא בתרחישים אסינכרוניים. המסך נפתח, המשתמש עבר לרקע, חזר — ורואה נתונים מיושנים, כי תגובת הרשת הגיעה לאחר ביטול הבקשה. פגמים כאלה לא משוחזרים באופן ידני בצורה יציבה: הם תלויים בסדר האירועים, במהירות הרשת ובמחזור החיים.
בדיקות יחידה ל-Android מתעדות בדיוק את הסדר הזה — מחליפות את הרשת ואת האחסון, מזינות תגובות ברצף הנדרש ובודקות שמצב המסך מגיע לצורה הצפויה.
צוואר הבקבוק השני — מצב המסך. טעינה, שגיאה, תוצאה ריקה והצלחה מתערבבים לעתים קרובות בדגל אחד, ובמעבר מהיר הממשק מציג שילוב שלא אמור להתקיים: ספינר מעל שגיאה או רשימה ריקה במקום נתונים מהמטמון. בדיקה של מעברי מצב תופסת שילובים כאלה לפני שהמשתמש רואה אותם.
השלישי — טיפול בשגיאות ומטמון. מה קורה בזמן פסק זמן, בתגובה לא תקינה, בחוסר סנכרון בין נתונים מקומיים לשרת? ללא כיסוי ההיגיון, הענפים האלה נשארים לא בדוקים, והמצב הלא שגרתי הראשון עובר לגרסה כנסיגה. בדיקות יחידה מריצות כל ענף בנפרד — ללא אמולטור ומכשיר, בשניות, ולכן ניתן להריץ אותן על כל שינוי.
המשמעות אינה באחוזי כיסוי לשם דיווח, אלא באילו מחלקות כשל מכוסות. אנחנו מכסים בראש ובראשונה תרחישים אסינכרוניים, מעברי מצב של המסך, עבודה עם מטמון וטיפול בשגיאות — מה שנשבר לרוב בעת שינויים. בהמשך נסביר כיצד זה בנוי בפרויקט.
מה נותן כיסוי בדיקות: אפקט מדיד
בדיקות יחידה ל-Android נותנות לצוות את מה שאף שלב של בדיקות ידניות לא נותן — משוב בתוך שניות במקום שעות.
בזמן שהבנייה וההרצה על האמולטור נמשכות דקות, מערך הבדיקות המודולריות על JUnit ו-MockK מסתיים בזמן השווה לזמן הקומפילציה, והמפתח לומד על תקלה מיד לאחר שמירת הקובץ, ולא בשלב הרגרסיה לפני הפרסום. זה משנה את מכניקת העבודה עצמה: שינוי בלוגיקה העסקית מפסיק להיות סיכון, כי ההשלכות שלו נראות מיד.
שיעור הפגמים שמתגלים לפני סקירת קוד גדל באופן צפוי — ערכי גבול, תשובות ריקות, שגיאות פענוח וענפי טיפול בחריגות מכוסים בבדיקות ברמת הפונקציות והמחלקות.
ככל שתסריטי ה-ViewModel ושכבת העבודה עם הנתונים מכוסים בצפיפות רבה יותר, כך פחות שגיאות מגיעות לשלב הבנייה, ופחות פעמים משחררת הגרסה מתעכבת בגלל ממצאים בבדיקות הקבלה.
יציבות השחרורים היא תוצאה ישירה. כאשר הלוגיקה מעוגנת בבדיקות, ריפקטורינג ועדכון תלויות מפסיקים להיות הגרלה: כל אי-התאמה בהתנהגות צפה בהרצה, ולא אצל המשתמשים. נדרשים פחות החזרות ופרסומים חוזרים, ומחזור השחרור עצמו מתקצר.
אנחנו בונים כיסוי במקומות שבהם הוא משתלם: תסריטים קריטיים, חישובים, עבודה עם רשת ואחסון — תחת בדיקות, ומסכים משניים לא מעכבים את עבודת הצוות בבדיקות מיותרות. תארו את המשימה — נחזור עם הערכה ותוכנית כיסוי לפרויקט שלכם.
גישות לבדיקות: השוואת אפשרויות
הבחירה בגישה נקבעת לפי מה בדיוק אנחנו בודקים וכמה יקר לנו לקבל משוב. בדיקות יחידה טהורות מבודדות את הלוגיקה ללא סביבת Android ומספקות תוצאה תוך שניות, אך אינן תופסות שגיאות חיבור בין שכבות.
בדיקות עם החלפת תלויות מכסות את האינטראקציה עם הרשת, האחסון ושירותי המערכת, ונשארות מהירות. בדיקות אינטגרציה מרכיבות רכיבים אמיתיים ומאשרות שהמודולים עובדים יחד — במחיר של זמן הרצה ושבריריות לשינויים בסביבה.
| גישה | מתי מתאימה | מגבלות |
|---|---|---|
| בדיקות יחידה טהורות | לוגיקה עסקית, חישובים, ניתוח תשובות, תנאי גבול | אינן בודקות תלויות אמיתיות והתנהגות הפלטפורמה |
| בדיקות עם החלפת תלויות | תרחישים עם רשת, מסד נתונים, שירותי מערכת; בדיקת הסתעפויות ושגיאות | ההחלפה עשויה לסטות מההתנהגות האמיתית של התלות |
| בדיקות אינטגרציה | חיבור בין שכבות, עבודה עם אחסון, תרחישים מקצה לקצה | הרצה ארוכה, רגישות לסביבה, קשה יותר לאתר כשל |
כיצד לשלב בין הגישות
הפרקטיקה היא פירמידה: בסיס של בדיקות יחידה טהורות על הלוגיקה העסקית, שכבה אמצעית עם החלפת תלויות על אינטראקציות, ופסגה של בדיקות אינטגרציה על מסלולים קריטיים. חלוקה כזו שומרת על הרצה מהירה ועל כיסוי משמעותי: רוב הפגמים נתפסים בשכבות התחתונות, שבהן זול יותר לתקן אותם.
מגבלות שחשוב לקחת בחשבון
אף גישה אינה מחליפה את האחרות. בדיקות טהורות לא יתפסו אי-תאימות של גרסאות ספריות, החלפה — סטייה של החוזה מהשירות האמיתי, ובדיקות אינטגרציה — מקרי גבול נדירים של הלוגיקה. לכן המערך נבנה לפי הסיכונים של האפליקציה הספציפית, ולא לפי תבנית.
כיצד אנו מטמיעים בדיקות: שלבי העבודה
הטמעת בדיקות מתחילה לא בכתיבת קוד, אלא במלאי: אילו מודולים בכלל קיימים, היכן מרוכזת הלוגיקה, והיכן — רק סימון מסכים. בשלב זה אנו בוחנים את היסטוריית השינויים ומוצאים את המחלקות שמתוקנות בתדירות הגבוהה ביותר: דווקא שם השגיאות יקרות ביותר, והבדיקות מניבות את האפקט הגדול ביותר.
- ביקורת קוד. מפרקים את מבנה הפרויקט, התלויות בין המודולים, איכות הפרדת השכבות. מסמנים מקומות שבהם הלוגיקה מעורבת עם פנייה לרשת או לבסיס הנתונים — אותם יהיה צורך להפריד קלות, אחרת לא ניתן לבודד את הבדיקה ללא מערכות חיצוניות.
- תכנית כיסוי. קובעים מה בדיוק בודקים: חישובים, המרת נתונים, טיפול בשגיאות, ערכי גבול. מסכמים על שיעור כיסוי יעד עבור מודולים קריטיים — לא "הכול ללא הבחנה", אלא במקום שבו מחיר השגיאה גבוה יותר.
- הגדרת סביבה. מקימים מתאר בדיקות בבנייה: מערכת משימות נפרדת, דוח כיסוי, הרצה אוטומטית על כל שינוי. בדיקות שצריך להריץ ידנית מפסיקים להריץ אחרי שבועיים.
- כתיבת בדיקות. הולכים מהקריטי למשני. כל בדיקה בודקת התנהגות אחת ואינה תלויה בסדר ההרצה. תלויות חיצוניות מוחלפות בתחליפים נשלטים, ולכן המערכת עוברת בשניות ואינה דורשת רשת.
- בדיקה על תרחישים אמיתיים. מריצים את המערכת על שינויים שכבר שברו את האפליקציה בעבר — מוודאים שהבדיקות תופסות אותם. בדיקה שתמיד ירוקה גרועה מהיעדר בדיקה: היא יוצרת ביטחון שווא.
- מסירה. מוסרים את המערכת, הוראות הרצה, כללים להוספת בדיקות חדשות וניתוח שגיאות טיפוסיות. מקיימים ניתוח עם הצוות שלכם, כדי שהתמיכה לא תהיה תלויה בנו.
קריטריון המפתח ביציאה — לא מספר הבדיקות, אלא הזמן עד גילוי הרגרסיה: השגיאה צריכה לצוף בבנייה, ולא בחנות האפליקציות. תארו את המשימה — נחזור עם הערכה ותכנית לפרויקט שלכם.
ארטיפקטים ותקנות: מה נשאר בפרויקט
לאחר מסירת הפרויקט, במאגר הקוד לא נשאר "אוסף בדיקות", אלא מעגל בדיקות עובד שאפשר להריץ ללא המחבר. להלן מה בדיוק מועבר לידיים ואיך משתמשת בכך צוות הלקוח.
- ערכות בדיקה לפי שכבות. הדומיין והלוגיקה העסקית מכוסים בבדיקות מהירות ללא תלויות Android, העבודה עם אחסון ורשת — בערכות נפרדות עם החלפת קריאות חיצוניות. כל ערכה מורצת באופן מבודד, ולכן כשל מאותר תוך שניות, ולא תוך הרצה של כל הפרויקט.
- קונפיגורציות הרצה. פרופילים מוכנים להרצה מקומית, לבדיקות בכל שינוי ולבנייה לילית עם כיסוי מלא. המפתח מריץ את הפרופיל הנדרש בפקודה אחת, ללא הגדרה ידנית של הסביבה.
- תרשימי תלויות. דיאגרמות המראות אילו מודולים תלויים במה והיכן עובר גבול ההחלפה. לפיהן ניתן לראות מדוע בדיקה נכשלת בשינוי של מחלקה מסוימת, והיכן להוסיף בדיקות חדשות.
- דוחות כיסוי. סיכום מוגדר לפי מודולים עם סף שמתחתיו הבנייה לא עוברת. ניתן לראות אילו ענפי לוגיקה לא נבדקו — זהו רשימת משימות, ולא מספר מופשט.
- תיעוד. מוסכמות למתן שמות לבדיקות, כללים לעיצוב נתוני בדיקה, ותיאור של מה נחשב לכיסוי מספק עבור קוד חדש.
- תקנות הרצה. מתי הבדיקות חובה, מה חוסם מיזוג שינויים, מי מטפל בבדיקות שנכשלו ובאיזה פרק זמן. הכללים מעוגנים במאגר הקוד, ולא בהתכתבות.
- הוראות תחזוקה. כיצד להוסיף בדיקה לפונקציונליות חדשה, כיצד לעדכן החלפות בעת החלפת שירותים חיצוניים, מה לעשות במקרה של בדיקה לא יציבה.
כל זה נמצא לצד הקוד ומנוהל בגרסאות יחד איתו. מהנדס חדש בצוות פורש את הסביבה ומקבל הרצה ירוקה ביום הראשון — ללא הסברים בעל פה מהמחבר.
מקרה בוחן: צמצום רגרסיות לאחר הטמעת בדיקות
הפרויקט — אפליקציית Android עם תשלומים ומנויים, שחרור אחת לשבועיים. בדיקות היו, אך נכתבו לסירוגין ובעיקר לפונקציות "טהורות": לוגיקת התשלום, חישוב ההנחות ושחזור המנוי נותרו ללא כיסוי.
רגרסיות נתפסו בהרצה ידנית לפני הפרסום, וכאחת מכל ארבע בערך הגיעה למשתמשים — לרוב בצורה של חיובים כפולים וסטטוסים "תקועים" של הזמנה.
מה שינו
הפרידו בין הלוגיקה לקוד הפלטפורמה: כללי העסק הועברו למחלקות נפרדות ללא תלויות בסביבת Android, ואת השירותים החיצוניים — תשלום, אחסון, רשת — עטפו בממשקים.
לאחר מכן, באמצעות MockK, תיארו את ההתנהגות של ממשקים אלה, כולל כשלים ופסקי זמן, ו-JUnit 5 סיפק בדיקות פרמטריות: מערך אחד של ערכי גבול במקום עשרה מתודות כמעט זהות.
הבדיקות רצות על כל קומיט, ולכן הבנייה נופלת לפני הריוויו, ולא אחריו. בנוסף, נקבע כלל: ענף חדש של לוגיקת תשלום לא עובר בדיקה אם אינו מכוסה בתרחישי שגיאה — לא רק בנתיב ההצלחה.
מדדים לפני ואחרי
| מדד | לפני | אחרי |
|---|---|---|
| שיעור הפגמים שהגיעו למשתמשים | ~25% | ~6% |
| בדיקה ידנית של השחרור | יומיים | 4-6 שעות |
| בניות שנפלו באינטגרציה | כל שלישית | בודדות |
| כיסוי לוגיקת התשלום | חלקי | תרחישים עיקריים ותרחישי שגיאה |
ההרצה הידנית הצטמצמה לא מפני שהבדיקות פחתו, אלא מפני שנתיבים קריטיים נחסמים כעת אוטומטית. הזמן שהתפנה הופנה לבדיקת מה שלא מכוסה בבדיקות: התנהגות במכשירים אמיתיים, עבודה עם דיאלוגים של תשלום, שחזור לאחר מזעור האפליקציה.
לאחר שני שחרורים שיעור הפגמים התייצב ברמה חדשה, והחזרת גרסה הפסיקה להיות תרחיש שגרתי.
במה נבדלים בדיקות יחידה מבדיקות אינטגרציה?
בדיקות יחידה בודקות מודול אחד בבידוד: כל התלויות החיצוניות — מאגרים, לקוחות רשת, שירותי מערכת — מוחלפות בכפילים מבוקרים. בדיקות אלו רצות על JVM, ללא אמולטור ומכשיר, ולכן כל המערכת של מאות תרחישים עוברת תוך שניות.
בידוד המודולים כאן אינו מטרה בפני עצמו: הוא מסיר כשלים שגויים, כאשר בדיקה נכשלת בגלל שרת לא זמין או קוד זר, ולא בגלל שגיאה בלוגיקה הנבדקת.
בדיקות אינטגרציה פועלות במפגש בין מודולים ובודקות שהם אכן מתחברים: סריאליזציה של בקשה ופענוח תגובה, התנהגות המטמון בעת שינוי נתונים, נכונות השאילתות למסד נתונים מקומי. כאן כבר עולה סביבה אמיתית — מסד נתונים לבדיקות, שרת מדומה, לעיתים אמולטור.
בדיקות אלו נמשכות זמן רב יותר ונכשלות לעתים קרובות יותר, אך תופסות סוג שגיאות שבדיקה מבודדת לא תראה כלל.
גבול הבדיקות עובר בשאלה "של מי האחריות". לוגיקת החישוב, ולידציה של שדות, פענוח מצבי מסך, המרת מודלים — אלו בדיקות יחידה, מכיוון שהתוצאה תלויה רק בנתוני הקלט.
אינטראקציה עם הרשת, מסד הנתונים, מערכת הקבצים, הרשאות ומחזור החיים — בדיקות אינטגרציה, מכיוון שהתוצאה תלויה בסביבה. אין לערבב אותן במערכת אחת: מאבדים את המהירות ואת דיוק איתור השגיאה.
בפועל המערכת נבנית כפירמידה: עיקר הבדיקות המבודדות המהירות על לוגיקה עסקית, מעליהן — בדיקות אינטגרציה על מפגשים קריטיים, ומעט תרחישים מקצה לקצה על מסלולי המשתמש העיקריים. חלוקה כזו מספקת משוב מוקדם בתהליך הפיתוח ואינה הופכת את ההרצה לפני שחרור להמתנה של שעות רבות.
אם הגבול מטושטש והכול נערם לבדיקות אינטגרציה, הצוות מפסיק להריץ אותן מקומית — והבדיקות מפסיקות להגן על הקוד.
ספקות טכניים נפוצות
יכולת הבדיקה של אפליקציית Android לעיתים רחוקות נתקלת במגבלה של עצם הפריימוורק — היא נתקלת במגבלה של הארכיטקטורה שהתגבשה עוד לפני שמישהו בכלל חשב על בדיקות. להלן ארבע שאלות שמנהלים טכניים שואלים בתדירות הגבוהה ביותר, וכיצד אנו עונים עליהן ברמת הקוד, ולא בסיסמאות כלליות.
יש לנו קוד לגאסי ללא בדיקות — במה אתם מתחילים?
תחילה מתעדים את ההתנהגות הנוכחית באמצעות בדיקות-מאפיינות: מכסים את התרחישים העמוסים ביותר כפי שהם פועלים כעת, כולל המוזרויות. זהו כיסוי ביטחון לפני ריפקטורינג — לאחר כל שינוי, ההרצה מראה שההתנהגות לא זזה.
לאחר מכן מוציאים את הלוגיקה מתוך האקטיביטי והפרגמנטים אל מחלקות נפרדות שניתן לבדוק ללא אמולטור: בדיקה על לוגיקה טהורה מתבצעת בשברירי שנייה, וכל ההרצה מפסיקה להיות צוואר בקבוק של הבנייה.
כיצד לבדוק אסינכרוניות וקורוטינות כך שהבדיקות לא "ימהבהבו"?
מחליפים את הדיספטשרים במתזמן בדיקות ומקדמים זמן וירטואלי באופן ידני — הבדיקה מפסיקה להיות תלויה בהשהיות אמיתיות ובזמני המתנה ברשת. קוד אסינכרוני נבדק באופן דטרמיניסטי: אותה הרצה בדיוק מניבה אותה תוצאה גם על המכונה המקומית וגם על שרת הבנייה. בנפרד מכסים ביטול קורוטינות וטיפול בשגיאות — דווקא שם לרוב מסתתרים הפגמים שלא נתפסים בבדיקה ידנית.
עד כמה עמוק אתם מחליפים תלויות?
אנו מבודדים את הגבולות החיצוניים — הרשת, מסד הנתונים, מערכת הקבצים, זמן המערכת — ומשאירים אמיתית את הלוגיקה שאנו בודקים. מוק על כל דבר קטן הופך את הבדיקה לשברירית: היא נופלת בריפקטורינג תמים ולא אומרת דבר על ההתנהגות. החלפת תלויות נבנית על ממשקים, ולכן הבדיקה שורדת החלפה של לקוח רשת או של אחסון ללא כתיבה מחדש.
מי מתחזק את הבדיקות לאחר מסירת הפרויקט?
בדיקות הן חלק מבסיס הקוד, ולא ארטיפקט נפרד: הן נמצאות לצד הקוד, עוברות סקירה באותו בקשת מיזוג, ומופעלות בכל שינוי. אנו מעבירים לצוות מוסכמות לגבי מבנה הבדיקות וניתוח של תרחישים טיפוסיים, כדי שמפתח חדש יכתוב אותן באותו סגנון. אם בדיקה מתיישנת יחד עם הדרישה — היא מתוקנת או נמחקת, ולא מושבתת לנצח.
תארו את בסיס הקוד שלכם ואת הכיסוי הנוכחי — נחזור עם הערכה ותוכנית, אילו אזורים לכסות בראשונה.
נדון במשימה ונחזור עם תוכנית
כדי לחזור עם תוכנית ולא עם מילים כלליות, אנחנו צריכים להבין את המבנה של פרויקט ה-Android שלכם ואת המצב הנוכחי של הבדיקות. שלחו גישה למאגר או ייצוא של המודולים — נבחן כיצד הקוד מחולק לשכבות, היכן עובר הגבול עם הרשת ועם האחסון, אילו תלויות מוזרקות דרך בנאים ואילו נמשכות באופן סטטי. מכך תלוי היכן בדיקות היחידה יתאימו ללא שכתוב קוד הייצור, והיכן יהיה צורך תחילה לנתק רכיבים תלויים.
בנפרד נבהיר את הדרישות: אילו תרחישים קריטיים עבורכם (חישוב לוגיקה עסקית, טיפול בשגיאות רשת, מטמון, ולידציה), איזו רמת כיסוי אתם מחשיבים למספקת והאם כבר קיימות בדיקות שאסור לשבור.
אם בפרויקט משתמשים במכל DI ובאסינכרוניות על קורוטינות, אמרו — עבורה מוגדר dispatcher לבדיקות, כדי שהבדיקות יתבצעו באופן דטרמיניסטי, ללא כשלים אקראיים.
מה נכין לשלב הבא:
- מפת מודולים ושכבות, שבהן הבדיקות נותנות את האפקט המרבי במינימום שינויים;
- רשימת מחלקות לבדיקה עם עדיפות לפי סיכון לרגרסיות;
- סכמת mocks עבור תלויות חיצוניות — רשת, אחסון, זמן מערכת;
- כללי בידוד לבדיקות, כדי שלא יהיו תלויות בסדר ההרצה;
- תצורת הרצה ב-build ודוח כיסוי;
- הערכת היקף העבודה לפי שלבים עם נקודות קבלה ביניים;
- תוכנית הטמעת בדיקות בצינור ה-build, כדי לתפוס רגרסיות לפני שחרור.
תארו את המשימה בצורה חופשית: מהי האפליקציה, באיזה שלב פיתוח היא נמצאת, מה כואב כרגע — רגרסיות תכופות, פחד מרפקטורינג או הכנה להעברת הקוד לצוות אחר. זה מספיק כדי לחזור עם הערכה, תוכנית עבודה והצעה לשלב הבא.







