אני משתדל בבלוג שלי להביא רעיונות חדשים ולא טריוויאלים.
במקרה הזה לא מדובר ברעיון חדש בכלל (הנה פוסט מ 2012) – אבל ככל הנראה בהחלט לא טריוויאלי.
לכאורה יש לקחים שהתעשיה שלנו לומדת, מטמיעה – וממשיכה הלאה, ויש כאלו שמלווים אותנו שנים ארוכות, בעוד שוב ושוב אנחנו חוזרים על אותן הטעויות.
אני יכול לתת כמה דוגמאות כאלו, אבל הפעם אתמקד באחת: שימוש שגוי ב Mock Objects.
האמת שהבעיה היא לא דווקא ב Mock Objects, כאשר אומרים “Mock Objects” הכוונה לרוב היא ל Stubs או ל Fakes – אבל ההבחנה לא חשובה. אני מדבר על השימוש המוגזם בכל סוגי ה Test Doubles.
לכאורה, כשלומדים לכתוב Unit Test מתחילים עם בדיקות פשוטות וישירות. כשרוצים “להתקדם” ומחפשים “מה אפשר לעשות מעבר?” מגיעים לעולם של Test Doubles – וה Frameworks השונים שעוזרים ליצור ולנהל אותם (כמו Mockito, SinonJS, MSW, ועוד עשרות), ונוצרת הרגשה שאנו “עושים משהו מתקדם יותר”.
באופן אירוני, שימוש ב Mock בבדיקות-יחידה היא אמנם טכניקה מתקדמת מעט יותר – אבל הנדסת תוכנה הרבה פחות טובה. קל יותר להתרשם מקוד מלא ב Mock מתוחכמים שקשה לעקוב אחרי כל הדקויות שלהם – מקוד פשוט שכל סטודנט מבין מיד.
דווקא המומחים בכתיבת בדיקות-יחידה ממעטים בלהשתמש Mocks, וזו מיומנות שכנראה לא טריוויאלי לרכוש.
האם Mocks הם תמיד רעיון רע?
ברור שלא.
אני אצמד להבחנה של Uncle Bob שמאוד נכונה בעיני:
- Mocks בין מערכות – הם חשובים, ואף נדרשים בכדי לבצע בדיקות בצורה יעילה.
- Mocks בתוך מערכת – הם טלאי (Patch). אפשר “לתפור” טלאי פה ושם, וזה לא רע. הבעיה היא כאשר המערכת שלנו הופכת לערימה ענקית של טלאים – וזה קורה לא מעט.
Mocks בין מערכות – הם חשובים, ואף נדרשים בכדי לבצע בדיקות בצורה יעילה
מהן “מערכות”? – לצורך העניין, נתחיל במיקרו-שירותים או מערכות צד-שלישי.
כאשר יש לנו בארגון כ 50 מיקרו-שירותים ואנו כותבים בדיקה המפעילה מספר של מיקרו-שירותים (נקרא לבדיקה כזו “System Test”) אזי:
- Scope הבדיקה הוא גדול: בדיקה בודדת מפעילה כנראה מאות או אלפי שורות של קוד.
- קשה מאוד להתמקד במקרי קצה בתוך ה Flow, והנטיה האנושית היא לא באמת לבדוק מקרי קצה.
- כשהבדיקה נופלת לא ברור לרוב מה נכשל – צריך להתחיל ולחקור. כלומר: כישלון של בדיקה מוביל לעבודה משמעותית נוספת – לפני שאפשר לתקן את הקוד.
- סביר יותר ויותר שזמני הריצה של הבדיקה יהיו גבוהים.
- נחשיב בדיקה שאורכת יותר מ 2 שניות – כבדיקה ארוכה. 2 שניות הן המון זמן מחשוב, אולי כדאי לחשוב עליהן כ 2,000,000,000 ננושניות – ולזכור שמחשבים בימנו מבצעים בננו-שנייה פעולה.
- כאשר יש לנו הרבה בדיקות (דבר טוב!) והבדיקות אורכות זמן רב => זמן ההמתנה לתוצאות הבדיקה אורך => תדירות הרצת הבדיקות פוחתת => גדל הזמן הממוצע מכתיבה של קוד שגוי – עד שאנו מגלים זאת => Feedback cycle ארוך יותר.
- “סטנדרט הזהב” להרצה של בדיקות טוען שהמתנה של יותר מ 10 דקות להרצה של בדיקות אינו סביר. לאחרונה אני רואה התפשרות על המדד הזה, ויש כאלו שגם מדברים על 15 דקות של הרצה כזמן סביר / רצוי.
Mocks בתוך מערכת – הם טלאי (Patch), שיש לצמצם את השימוש בו.
כל פעם שאנחנו משתמשים ב Mock לבדיקת קוד בתוך השירות הבודד (להלן “בדיקת יחידה”) – סימן שהקוד קשה מדי לבדיקה, ואנו נאלצים להעזר בטלאי.
לרוב הבעיה נובעת מכך שאין הפרדה בין:
- לוגיקה שהשירות מבצע – להלן “Pure Business Logic” (הכתובים כ Pure functions, כמובן)
- לוגיקה של תקשורת עם שירותים אחרים – להלן “Integration Logic”.
- דורשת מודעות ותשומת לב.
- מוסיפה מעט עבודה בעת הקידוד (נאמר: 10-15%)
- משפרת את המודולוריות (ומכאן – ה Design) של הקוד
- מאפשר לבדוק אותו בצורה יעילה הרבה יותר, הן מבחינת עומק הבדיקות, והן מבחינת זמן שמושקע בכתיבת בדיקות.
ברוב המקרים, ההשקעה בהפרדת הקוד תחזיר את עצמה כבר לאחר סיום כתיבת הבדיקות (בדיקות פשוטות יותר = חסכון זמן בעת כתיבת הבדיקות), ובוודאי ובוודאי שתחזיר את עצמה לאורך זמן – כאשר המערכת צריכה לעבור שינויים משמעותיים.
- היא רצה ומצליחה!
- אם מחקתי כמה שורות קוד בפונקציה הנבדקת ()doSomething – היא נכשלת. כלומר: היא בודקת משהו.
- השתמשתי ב mocks frameworks בצורה יעילה – וחסכתי המון קוד לו הייתי כותב את ה Mocks בעצמי.
- משתנה התנהגות במערכת – אולי נצטרך לשנות את הבדיקה ואולי לא.
- משתנה מבנה המערכת – כמעט בטוח שנצטרך לשנות את הבדיקה, ואולי עוד רבות אחריה.
- מוטיבציה נמוכה לבדיקת מקרי קצה – כי כתיבת כל מקרה קצה דורשת עדכון (ותחזוקה לעתיד) של עוד ועוד Mocks.
- צורך בתחזוקה שוטפת של ה Mocks: כל הוספה של פרמטר או שכבה לוגית – דורשת של עדכון של עוד ועוד בדיקות.
- זמני ריצה ארוכים יותר של הבדיקות
- נטיה לכתוב קוד בדיקה מתוחכם (“Mocking Sophistication”) שמקשה על קריאת קוד הבדיקה.
סיכום
- בדיקות של התנהגות של מיקרו-שירות – השתמשו ב Mocks.
- בדיקות של הלוגיקה בתוך המיקרו-שירות – המנעו מ Mocks. אפשר פה ושם.