ישנה מצגת מפורסמת של טובי קלמסון מ 2014, שמפרטת בדקדוק כיצד בודקים מיקרו-שירותים. עברתי עליה בקפדנות פעמיים – ולא ראיתי שום רעיון חדש.
בכל זאת נראה שאנשים רבים עושים הערכה מחדש לאסטרטגיית הבדיקות שלהם בהקשר של מיקרו-שירותים, וזה טוב לרענן אסטרטגיות מדי-פעם. אולי משהו השתנה בשנים האחרונות?
אני אשתף בפוסט את אסטרטגיית הבדיקות המועדפת עלי, ואחבר אותה לעולם מיקרו-שירותים.
לא גלידה, לא פירמידה – אלא יהלום
כשעובדים ללא אסטרטגיה, יש נטייה להגיע למודל של גלידה – מודל לא יעיל בעליל. המודל המוצלח יותר (קצת הגזמנו עם “אידאלי”, לא?) – הוא ההופכי לחלוטין.
חשוב מאוד וטוב לחתור למשהו שדומה יותר לפרמידה. אני מניח שאין צורך להסביר מדוע.
המודל המועדף עלי הוא דווקא מודל היהלום:
לאחר שנים שהייתי חסיד של מודל הפירמידה, וריבוי בכתיבות 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”) ברורים, וכל רכיב בדוק כראוי – ניתן כבר להשיג רמת אמינות מרשימה של המערכת.
- אמנם אין בתרשים הרבה שירותים – אנא הניחו שמדובר במערכת מלאה (עשרות Services ויותר), עם תצורה קרובה ככל האפשר ל Production, עם האפליקציה / UI, ושירותים חיצונים בהם משתמשים (למשל: שירות לשליחת הודעות SMS).
- המטרה: לפני שמשחררים שינוי משמעותי לפרודקשיין, נרצה להשקיע בבדיקה ולאתר תקלות לפני ההגעה לפרודקשיין, בסביבה אמיתית ומלאה ככל האפשר.
- בבדיקות אינטגרציה בודקים כמה שירותים כשהם עובדים ביחד. בדרך כלל זו קבוצה קבועה של שירותים הנמצאים בקשר עמוק זה עם זה, מה שניתן גם לכנות גם: “sub-system”.
- לפני הרצת בדיקת E2E – מריצים את בדיקת האינטגרציה המתאימה לשירות / תסריט (אם יש כזו). הקמת הסביבה היא מהירה וזולה יותר (נניח 2 עד 7 שירותים – במקום עשרות רבות של שירותים). הבדיקות רצות מהר יותר והן ממוקדות יותר (כלומר: סבירות גבוהה יותר לאתר תקלות).
- עבור שינוי קטן או בינוני באחד השירותים, ניתן להסתפק בהרצה של ה sub-system הרלוונטי. עניין של בחירה וניהול סיכונים.
- ב Sub-System שבתרשים – שירות A הוא “המוביל” ותמיד דרכו ייעשו הבדיקות, גם כאשר השינוי שנבדק הוא בשירות אחר.
- לא נשלח באמת SMSים, אלא נבצע Mock לשירות החיצוני.
- ייתכן ואחד השירותים שלנו ב Sub-system הוא מורכב מדי להפעלה / קונפיגורציה – ולכן גם אותו נחליף ב Mock. כדאי להימנע מגישה זו במידת האפשר.
סיכום
קישורים רלוונטיים
First Class Tests – מאמר דעה של הדוד בוב בנושא, הוא הרי חסיד של בדיקות יחידה קפדניות ו TDD.