אני משתדל בבלוג שלי להביא רעיונות חדשים ולא טריוויאלים.
במקרה הזה לא מדובר ברעיון חדש בכלל (הנה פוסט מ 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 דקות של הרצה כזמן סביר / רצוי.
מכאן, אפשר לכתוב הרבה בדיקות, שירוצו הרבה זמן – ולהתדרדר ב Feedback cycle של המפתח.
הפתרון הברור (וכמעט היחידי) הוא להקדיש את רוב הבדיקות ליחידה קטנה יותר של המערכת: מיקרו-שירות בודד. כואב לי לחשוב כמה סבל אנושי מצטבר לפספוס הנקודה הזו. לעתים בדובר בשנות-אדם רבות, ברמת הארגון הבודד. אאוץ!
אי אפשר לבדוק מיקרו-שירות ברצינות בלי שהוא יקרא לשירותים שהוא תלוי בהם. לכן חייבים לכתוב Mocks שידמו את המערכת / מיקרו-שירותים האחרים שהשירות שלנו תלוי בהם – בזמן שבודקים את השירות.
ה Scope המצומצם של בדיקת מיקרו-שירות בודד – רק תשפר לנו את המדדים החשובים:
יכולת התמקדות הבדיקה במקרי קצה, זמני איתור תקלה, וזמני הריצה של הבדיקה.
כמובן שנכון לשמור גם על כמות מסוימת של System Tests שיבדקו את האינטגרציה בין שירותים שונים. לבדוק שהם ממשיכים לדבר באותה שפה.
Mocks בתוך מערכת – הם טלאי (Patch), שיש לצמצם את השימוש בו.
כל פעם שאנחנו משתמשים ב Mock לבדיקת קוד בתוך השירות הבודד (להלן “בדיקת יחידה”) – סימן שהקוד קשה מדי לבדיקה, ואנו נאלצים להעזר בטלאי.
לרוב הבעיה נובעת מכך שאין הפרדה בין:
לוגיקה שהשירות מבצע – להלן “Pure Business Logic” (הכתובים כ Pure functions, כמובן)
לוגיקה של תקשורת עם שירותים אחרים – להלן “Integration Logic”.
ההפרדה הזו קלה בעת כתיבת קוד חדש – וכמעט בלתי אפשרית על גבי קוד קיים שכתוב כך.
כאשר עושים את ההפרדה – קל לכתוב בדיקות יחידה בלי Mocks.
כאשר לא עושים את ההפקדה – קשה מאוד לכתוב בדיקות יחידה, ואז מגיע שימוש מופרז ב Mocks.
ככל אצבע, אני מחשיב שימוש ב Mocks כמופרז אם יותר מ 10% מבדיקות היחידה שלנו משתמשות ב Mocks.
אני לא מתכוון להמליץ פה לקחת קוד קיים ולבצע הפרדה בין הקוד. זו מלאכה קשה, ארוכה – ולא מתגמלת.
אני ממליץ בחום לכתוב את כל הקוד החדש שלכם עם כזו הפרדה. זה נכון כמעט לכל סיטואציה.
העבודה הנוספת בהפרדה בין לוגיקה עסקית ללוגיקה של אינטגרציה:
דורשת מודעות ותשומת לב.
מוסיפה מעט עבודה בעת הקידוד (נאמר: 10-15%)
אבל:
משפרת את המודולוריות (ומכאן – ה Design) של הקוד
מאפשר לבדוק אותו בצורה יעילה הרבה יותר, הן מבחינת עומק הבדיקות, והן מבחינת זמן שמושקע בכתיבת בדיקות.
ברוב המקרים, ההשקעה בהפרדת הקוד תחזיר את עצמה כבר לאחר סיום כתיבת הבדיקות (בדיקות פשוטות יותר = חסכון זמן בעת כתיבת הבדיקות), ובוודאי ובוודאי שתחזיר את עצמה לאורך זמן – כאשר המערכת צריכה לעבור שינויים משמעותיים.
מה הבעיה בשימוש ב Mocks בבדיקות -יחידה?
הנה דוגמה טיפוסית ל Heavily mocked test, ראיתי אינספור כאלו בחיי – ואראה כנראה (אולי הפוסט יעזור?) עוד אינספור בעתיד:
מה הבעיה בבדיקה הזו?
היא רצה ומצליחה!
אם מחקתי כמה שורות קוד בפונקציה הנבדקת ()doSomething – היא נכשלת. כלומר: היא בודקת משהו.
השתמשתי ב mocks frameworks בצורה יעילה – וחסכתי המון קוד לו הייתי כותב את ה Mocks בעצמי.
מה עוד אפשר לבקש?!
יש בבדיקה הזו, או בדפוס של הבדיקה הזו כמה בעיות חמורות. לרוע המזל – אלו לא בעיות שיצוצו מחר, אלא טיפוסי יותר שיצוצו עוד שנה – לאחר שכתבנו עוד מאות בדיקות כאלו, והתחפרנו / קיבענו חזק יותר – את בעיה.
בעיה: לא ברור מה בדיוק נבדק, מה הצלחת הרצה של הבדיקה – באמת אומרת.
כשאני קורא את קוד הבדיקה, גם בלי obfuscation ושמות משמעותיים – אני מבין שבוצעה פעולה, אבל אני לא יכול לדעת מה חלקה של הפונקציה ()doSomething בעבודה – ומה חלקם של ה Mocks שלה.
הדרך היחידה שלי להבין מה החלוקה, ומה באמת ()doSomething עושה לאחר שמסירים ממנה את ה Mocks – היא להיכנס לקוד ולקרוא אותו. לפי מספר ה mocks אפשר לנחש כמה זה יהיה קל. הרבה פעמים קריאה שטחית – מפספסת חלק מהעניין.
גם כאשר אני כותב בדיקה בתצורה הזו והיא הגיונית, לאורך זמן ושינויים (refactorings במערכת) – יש סיכוי שהיא תאבד את המשמעות שלה.
שוב ושוב ושוב נתקלתי בבדיקות מהסוג הזה שהיו קליפת שום ריקה – שלא בדקו שום דבר. זה נראה מצחיק ומגוחך שכל שאני יוצר Mock עם ערך x ואז מריץ בדיקה ששולפת את x ומראה ש x == x, אבל זה קורה גם לאנשים חכמים שמבינים קוד.
כאשר עושים refactoring במערכת – אי אפשר להבין אלו בדיקות Mock Heavy עומדות לאבד את ערכן.
כאשר הבדיקות הללו נשברות ומתקנים אותן כחלק משינוי – קשה מאוד לוודא שאנחנו משמרים את הערך שלהם. הכלל בגלל שמה שנבדק הוא משתמע ואינו גלוי.
לכן, זו היא בעיה בתהליך / בתבנית – ולא בקוד הספציפי.
בעיה: הבדיקה בודקת איך דברים קרו (מבנה), לא מה קרה (התנהגות).
בעצם הבדיקה בודקת שכאשר מפעילים את ()doSomething נקראות פונקציות כאלו וכאלו במערכת, עם פרמטרים מסוימים ו/או ערכים מסוימים ו/או לא נקראות פונקציות אחרות.
לא ברור לנו אם בסוף, קצה לקצה, הלקוח קיבל את ההנחה שרצינו.
בקלות, אפשר לשמור את סדר הקריאות (המבנה), אבל להיכשל בתוצאה (התנהגות).
“האא! הבדיקות לא גילו את זה כי זה היה באג ב SQL” – הוא סוג התירוץ שאנו מספרים לעצמנו במקרים האלו. “אולי כדאי להוסיף גם בדיקה גם על מבנה השאילתא” (בבקשה: לא!)
כאשר:
משתנה התנהגות במערכת – אולי נצטרך לשנות את הבדיקה ואולי לא.
משתנה מבנה המערכת – כמעט בטוח שנצטרך לשנות את הבדיקה, ואולי עוד רבות אחריה.
מצב איום שאפשר להגיע אליו, הוא שכאשר אנחנו רוצים לעשות Refactoring משמעותי במערכת – רבות מהבדיקות הללו ישברו. ייקח לנו זמן רב לתקן את כולן, מעין “יום עבודה לבצע Refactoring – ושבועיים עבודה לתקן את כל בדיקות”.
כאשר נבצע שינוי מבנה, הבדיקות לא ישרתו אותנו בבדיקת רגרסיה של התנהגות – כי הן נשברו בגלל שינוי המבנה.
הבדיקות הללו מעבירות אותנו סדנאת חינוך איומה: לא כדאי לשנות את מבנה המערכת. המערכת הזו “בדוקה היטב” (חחחח), אך היא לא אוהבת שינויים.
קוד שלא מתחדש – הוא קוד גוסס. דפוס הבדיקות הללו עוזר לקוד לגסוס זמן קצר לאחר שנכתב לראשונה.
בעיות נוספות
בעיות נוספות הן:
מוטיבציה נמוכה לבדיקת מקרי קצה – כי כתיבת כל מקרה קצה דורשת עדכון (ותחזוקה לעתיד) של עוד ועוד Mocks.
צורך בתחזוקה שוטפת של ה Mocks: כל הוספה של פרמטר או שכבה לוגית – דורשת של עדכון של עוד ועוד בדיקות.
זמני ריצה ארוכים יותר של הבדיקות
נטיה לכתוב קוד בדיקה מתוחכם (“Mocking Sophistication”) שמקשה על קריאת קוד הבדיקה.
כל אלו הן בעיות אמיתיות, אבל הן מחווירות מול הנזק שבכתיבת קוד שאינו נבדק לעומק, ומקשה על ביצוע שינויי עומק במערכת. שוכחים מכיב קיבה – כשיש סרטן.
לגיקים שבינינו: הכוונה ל Port = “נמל”. לא IP Address port 🙂
סיכום
מפתיע אותי כמה נפוצים הפספוסים והבלבול באיזור הזה. כמה אנרגיה מושקעת בכתיבת בדיקות – שיקשו יותר ממה שהן עוזרות.
הכלל פשוט:
בדיקות של התנהגות של מיקרו-שירות – השתמשו ב Mocks.
בדיקות של הלוגיקה בתוך המיקרו-שירות – המנעו מ Mocks. אפשר פה ושם.
הישום פשוט בקוד קיים, ומאוד קשה בקוד שכבר נכתב בצורה שלא תומכת בהפרדה הזו.
עצוב לראות כמה פעמים ארגונים הבינו את העניין הפוך, והשתמשו ב Mocks בעיקר בבדיקות יחידה ו/או כמעט לא בבדיקות מערכת.
חבל לראות את כל הקוד שנכתב בלי הפרדה של לוגיקה – מה שיהפוך את בדיקות היחידה לקשות יותר וליעילות פחות.
במקרים כאלו, אני אפילו מבין לליבם של “המתנגדים לבדיקות-יחידה”: כשכותבים אותן כך – אולי באמת עדיף בלי. לפחות אין את האשליה שהמערכת בדוקה ובטוח / קל לבצע בה שינויים.
בכל זאת נראה שאנשים רבים עושים הערכה מחדש לאסטרטגיית הבדיקות שלהם בהקשר של מיקרו-שירותים, וזה טוב לרענן אסטרטגיות מדי-פעם. אולי משהו השתנה בשנים האחרונות?
אני אשתף בפוסט את אסטרטגיית הבדיקות המועדפת עלי, ואחבר אותה לעולם מיקרו-שירותים.
לא גלידה, לא פירמידה – אלא יהלום
המודל הקלאסי של בדיקות אוטומציה הוא מודל “פירמידת הבדיקות”:
כשעובדים ללא אסטרטגיה, יש נטייה להגיע למודל של גלידה – מודל לא יעיל בעליל. המודל המוצלח יותר (קצת הגזמנו עם “אידאלי”, לא?) – הוא ההופכי לחלוטין.
חשוב מאוד וטוב לחתור למשהו שדומה יותר לפרמידה. אני מניח שאין צורך להסביר מדוע.
המודל המועדף עלי הוא דווקא מודל היהלום:
לאחר שנים שהייתי חסיד של מודל הפירמידה, וריבוי בכתיבות Unit Tests – השתכנעתי שמודל היהלום הוא יעיל יותר.
ההבחנה של ההבדלים בין בדיקות Integration, Component, ו E2E – היא משנית. העיקר הוא:
יש לנו מעט בדיקות ידניות: על מנת לא לשחוק, לא לעשות עבודה רוטינית שוב-ושוב-ושוב – אלא להשתמש במוח האנושי שאין לו תחליף, לזהות דברים לא טובים במערכת. (ויש אנשים שעושים זאת טוב יותר מאחרים).
יש לנו כמות בינונית של בדיקות יחידה:
בדיקות יחידה הן מעולות (!!) לבדיקת Pure Business Logic, קרי parsers, business rules, אלגוריתמים וכו’ – כל מה שיש לו מחזור: קלט – הרבה עבודה לוגית – פלט. ניתן לזהות ביתר קלות אזורי קוד כאלה בעזרת צפיפות של משפטי if ו for (בשפות המתאימות).
בדיקות יחידה הן פחות יעילות לקוד אינטגרציה (“עשה א, ואז עשה ב, ואז עשה ג” – כמו שליפת נתונים מבסיס נתונים).
בדיקות יחידה הן דיי לא יעילות ל UI.
בקיצור: נשתמש בהן ב sweet spot שלהן בלבד: Pure business logic.
הדגש של המודל הוא על בדיקות Component (לעתים נקראות אינטגרציה, או API) – הבודקות התנהגות של כל שירות בפני עצמו. בבדיקה פשוטה אפשר לכסות הרבה מאוד קוד, בסביבה יחסית מציאותית, מה שמייצר מעין sweet spot של עלות-תועלת: בין כמות ההשקעה בבדיקה – והערך שהיא מחזירה.
העקרונות של הפירמידה עדיין נשמרים ביהלום:
כמה שעולים למעלה הבדיקות הן: איטיות יותר להרצה, דורשות יותר תחזוקה –> יקרות יותר, ולכן ממעיטים בהן.
כמה שיורדים למטה הבדיקות הן ספציפיות יותר, רצות מהר יותר, ותלויות פחות בסביבה / אמינות יותר.
מה עם סוגי הבדיקות השונים?
ישנה חוסר סטנדרטיזציה ברורה ובעייתית בשמות בהן משתמשים לתיאור בדיקות שונות: מהו API Test ומהו Integration Test? – רבים לא יסכימו על ההגדרה. אני אצמד למונחים שמקובלים ב Gett, לא כי הם “בהכרח הטובים ביותר”, אלא כי אני רגיל אליהם כרגע.
להלן ההסברים של סוגי הבדיקות השונות, שישפכו גם אור על ההבדלים ביניהם.
לצורך הדיון, כך נראה שירות:
תקשורת בינו ובין שירותים אחרים מתבצעת על גבי HTTP (סינכרונית) או על גבי RabbitMQ (אסינכרונית)
ה flow המרכזי של השירות מופעל ע”י איזה Invoker מסתורי – שעדיף לא לחשוב עליו לפרטים, כשכותבים את הבדיקות. הוא עלול לעשות כל מה שה API מאפשר. לרוב זה יהיה שירות אחר.
הנה כמה דוגמאות לבדיקות יחידה:
המחלקה A היא מחלקה “חברה” של מחלקה X – הבודקת אותה. זוהי בדיקת pure unit tests, האידאל של בדיקות יחידה.
המחלקה B היא מחלקה “חברה” של מחלקה Y – הבודקת אותה. מכיוון ש Y תלויה ב X, ואנו רוצים בדיקה “טהורה”, קרי: נקודתית ומבודדת – אנו יוצרים Mock של X וכך מריצים את הבדיקה B על המחלקה Y בלבד.
מחלקה C בודקת את המחלקה Z, אבל גם את החברה האחרת שלה – מחלקה X. לכן היא נקראת sociable unit tests. היא חברותית.
כמה שבסיס הקוד שנבדק ע”י בדיקת יחידה הוא גדול יותר (יותר branching של ה flow – בעיקר), בדיקת היחידה היא יעילה פחות: יהיה קשה יותר לבדוק מקרי קצה, וכשלון של בדיקות יצביע בצורה פחות מדוייקת על מקור התקלה.
ככלל, אנו מעדיפים בדיקות pure unit tests על פני ב sociable unit tests – אבל זו הבחנה שלא תמיד מדייקת. למשל: אם עשינו refactoring למחלקה גדולה Z, והוצאנו ממנה קוד למחלקה חדשה X – אזי הבדיקה C הפכה ל sociable unit tests. למרות זאת, היא טובה בדיוק באותה המידה כפי שהייתה לפני ה Refactoring.
הערה: בתרשים נראה שאני בודק את כל המחלקות שיש לי, בפועל כנראה שאבדוק בעזרת בדיקות יחידה רק 10-30% מהמחלקות ב Service (תלוי כמובן בשירות)
המאסה העיקרית של האוטומציה מתבצעת ע”י בדיקות Component – הבודקות רכיב בודד במערכת.
במקרה של MSA, הרכיב הוא שירות, עם ה Database שלו – אך ללא תלויות בשירותים חיצוניים.
הבדיקות מתבצעות רק דרך ה APIs של השירות, אם כי פעמים רבות מייצרים נתונים ב Database לפני הבדיקה ישירות דרך ה Models של השירות עצמו (כך שקל לייצר נתונים עקביים, בפורמט המעודכן ביותר).
המטאפורה של השם Component היא כמו של רכיב אלקטרוני. המערכת עשויה להיות מחשב ויש לה ערך עסקי רק כשהיא שלמה, אבל כל רכיב (Component) נבדק ביסודיות בפני עצמו: זיכרון, דיסק, מעבד, וכו’.
יצרני הזיכרון, למשל, בודקים את הרכיב שלהם בקנאות – בודקים שהוא עובד, גם בכל מצבי הקצה, כפי שמצופה ממנו. כאשר הממשקים (“contracts”) ברורים, וכל רכיב בדוק כראוי – ניתן כבר להשיג רמת אמינות מרשימה של המערכת.
כמובן שאי אפשר להסתמך רק על בדיקת הרכיבים העצמאיים. אנו עושים בדיקות של כלל המערכת (“מפעילים את המחשב”).
בעולם הפיסי מתחילים אולי בהדלקת המחשב, לראות שלא יוצא ממנו עשן (להלן “Smoke Tests”) – אך ממשיכים בבדיקות יותר מקיפות הבודקות את כלל המערכת (למשל: לגלוש ב 10 טאבים במקביל בדפדפן, תוך כדי שתוכנה להמרת וידאו רצה ברקע).
במונחים שלנו מדובר על End-To-End Tests, ובקיצור E2E Tests (מה שלעתים קרוי גם System Test):
אמנם אין בתרשים הרבה שירותים – אנא הניחו שמדובר במערכת מלאה (עשרות Services ויותר), עם תצורה קרובה ככל האפשר ל Production, עם האפליקציה / UI, ושירותים חיצונים בהם משתמשים (למשל: שירות לשליחת הודעות SMS).
המטרה: לפני שמשחררים שינוי משמעותי לפרודקשיין, נרצה להשקיע בבדיקה ולאתר תקלות לפני ההגעה לפרודקשיין, בסביבה אמיתית ומלאה ככל האפשר.
ואכן בדיקות E2E הן יקרות, וכדאי למעט בהן.
כאן מגיע התפקיד של בדיקות אינטגרציה: לאזן קצת בין עלות לתועלת:
בבדיקות אינטגרציה בודקים כמה שירותים כשהם עובדים ביחד. בדרך כלל זו קבוצה קבועה של שירותים הנמצאים בקשר עמוק זה עם זה, מה שניתן גם לכנות גם: “sub-system”.
לפני הרצת בדיקת E2E – מריצים את בדיקת האינטגרציה המתאימה לשירות / תסריט (אם יש כזו). הקמת הסביבה היא מהירה וזולה יותר (נניח 2 עד 7 שירותים – במקום עשרות רבות של שירותים). הבדיקות רצות מהר יותר והן ממוקדות יותר (כלומר: סבירות גבוהה יותר לאתר תקלות).
עבור שינוי קטן או בינוני באחד השירותים, ניתן להסתפק בהרצה של ה sub-system הרלוונטי. עניין של בחירה וניהול סיכונים.
ב Sub-System שבתרשים – שירות A הוא “המוביל” ותמיד דרכו ייעשו הבדיקות, גם כאשר השינוי שנבדק הוא בשירות אחר.
לא נשלח באמת SMSים, אלא נבצע Mock לשירות החיצוני.
ייתכן ואחד השירותים שלנו ב Sub-system הוא מורכב מדי להפעלה / קונפיגורציה – ולכן גם אותו נחליף ב Mock. כדאי להימנע מגישה זו במידת האפשר.
סיכום
זהו, זו הייתה סקירה מהירה משהו.
אני מקווה שהיא מובנת, ויכולה לחדש לקוראים רבים ככל האפשר.
שיהיה בהצלחה!
—–
קישורים רלוונטיים
First Class Tests – מאמר דעה של הדוד בוב בנושא, הוא הרי חסיד של בדיקות יחידה קפדניות ו TDD.