2017-04-03

כיצד בודקים מיקרו-שירותים? בעזרת יהלום הבדיקות, כמובן! (פוסט מאויר)

שאלו אותי פעם איך מבצעים בדיקות למיקרו-שירותים. "כמו כל קוד אחד" - עניתי בטבעיות. למה שמשהו השתנה?

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

כמובן שאי אפשר להסתמך רק על בדיקת הרכיבים העצמאיים. אנו עושים בדיקות של כלל המערכת ("מפעילים את המחשב"). 
בעולם הפיסי מתחילים אולי בהדלקת המחשב, לראות שלא יוצא ממנו עשן (להלן "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.



10 תגובות:

  1. לדעתי (עוד) מאמר מעולה. אני אגב, בא מתחום הבדיקות, ויש משהו שאני פחות "אוהב" בגישה, וזה המיעוט (היחסי, ברור) של בדיקות קצה לקצה. מצד אחד אם זה עובד לכם - מצויין. אבל מבלי להכיר את המערכת אני יכול לשער שיש מקרים שדווקא ש-flow בין כמה סאב סיסטמס יגרמו לבעייה. וכמובן עשויים להיות flows שהולכים בין סאב קומפונטות, חוזרים אליהן וכד'.
    אני לא מוריד ממה שאמרת, אלא הייתי מוסיף אוטומציה ועומסים ויציבות וכד' דוודא לסיסטם.
    אגב E2E, מה עם בדיקות דוגמת "כאוס מנקי" של נטפליקס שכוללות גם הורדה של סאב-סיסטמם וכד'?
    ולבסוף (לא יכלתי להתאפק) בתרשים היהלום שלך כתבת M(nual) במקום M(anual) אני מניח.

    השבמחק
    תשובות
    1. תודה דורון על התגובה!

      בדיקות כאוס, אנחנו מתכננים לעשות - אנחנו עדיין לא שם.
      אפרופו עשינו פעם בעבר וגילינו שהורדת מערכות זו סימולציה לא טובה: ההתנהגות הבעייתית היא לרוב עומס גדול (latency בתשובות) ו/או אחוז שגיאות גבוה - ואותם מנגנונים שהתנהגו יפה כשחלק מהמערת לא הייתה זמינה, לא התנהגו כ"כ יפה כאשר היו בעיות עומס / errors. הכוונה היא להשתמש במשהו כמו Toxy (קישור: https://github.com/h2non/toxy) על מנת לדמות את המצבים הללו.

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

      מחק
  2. איך זה משתלב עם TDD / BDD ? או שזה לא נהוג בעולם המיקרו-שירותים (מאז ומעולם עבדתי רק עם ממערכות מונוליתיות...)

    השבמחק
    תשובות
    1. משתלב מצוין. אלו דברים אורתוגונליים.
      רק לוודא: BDD ו TDD הם רעיונות אולי משלימים - אך בלתי קשורים.

      מחק
  3. אהלן ליאור, אחלה כתבה, השכלתי.

    אתה יכול אולי לתת קצת יותר דגש על בדיקות component כי לפי מה שרשמת בתרשים שלך זה הבדיקה הכי חשובה אבל לא מוקדש מספיק דגש בכתבה לגבי הבידקה עצמה. הגדרה של קומפוננטה זה משהו מאוד מאוד מופשט, אני מגיד מתחום ה-FE ומבחינתי component זה פשוט class שמכיל html +css + לוגיקה משלו. (יש לו גם הרבה תלויות ב- services ו- other classes). יתר על כן עם אני לוקח את זה לכיוון של smart ו- dumbcomponent אז מבחינתי יכול להיות מספר חלוקות למה שאתה קורה בדיקות component. האם זה בדיקת כל component בפני עצמה? שכן מסך מסוים נגיד dialog מכיל לי מספר קומפוננטות עם קשרים בינהם אז האם כל הדיאלוג בעצמו זה בדיקת קומפוננטה בעצמה? האם תוכל להכווין אותי יותר לגבי מהי בדיקת קומפוננטה יחידה ואיפה נמתח הגבול של בדיקת קומפוננטה לבין בדיקה אחרת :)

    תודה רבה

    השבמחק
    תשובות
    1. היי חן,

      נראה לי שנוצרה פה אי-הבנה:

      המודל הנ"ל מתאר בדיקות צד-שרת בלבד, ולא מבנה של בדיקות צד-לקוח. בדיקות צד-לקוח הוא דבר שבכלל לא נגעתי בו - והוא כנראה שונה מהותית.
      Component הכוונה מיקרו-שירות עצמאי, הנבדק דרך ה API שלו.

      מקווה שהבהרתי,
      ליאור

      מחק
  4. אנונימי9/4/17 17:43

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

    השבמחק
    תשובות
    1. היי אנונימי,

      זה מאוד תלוי בארגון....
      בדיקות יחידה - רק מפתחים יכולים לבצע.
      בדיקות Component - ראיתי מודלים שבהם מפתחים מבצעים את הבדיקות וראיתי כאלו שבהם אנשי אוטומציה מבצעים את הבדיקות - שני המודלים עבדו.
      בדיקות Integration ו E2E - כנראה שמקובל יותר שאנשי אוטומציה מבצעים.

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

      מחק
  5. לגבי בדיקות יחידה,
    לפי התיאור שאתה נותן, בבדיקות יחידה יש צמידות של מחלקה לבדיקות שלה.
    וזה יוצא בעייתי בכך שקוד הבדיקות תלוי במחלקה באופן מידי חזק, tightly coupled.
    מרטין פאולר מביא שיטה לפיה בדיקות יחידה בודקות יחידות התנהגותיות באופן בלתי תלוי בחלוקה למחלקות
    https://martinfowler.com/bliki/UnitTest.html
    מרטין בוב מדגיש את החשיבות של הורדת התלות הישירה בין המבחנים לקןד.
    http://blog.cleancoder.com/uncle-bob/2017/03/03/TDD-Harms-Architecture.html

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

    השבמחק
    תשובות
    1. היי רבקה,

      תודה על התגובה!

      "הצמידות" שאני מדבר עליה באה על מנת לתת כיוון לרזולוציה של הבדיקות: אל תבדקו הרבה קוד בעזרת בדיקות-יחידה, אלא מעט. איך יודעים מה זה "מעט"? על כל מחלקה של קוד (לוגי) הקציבו מחלקה של בדיקות יחידה - וכך תגיעו לגודל קטן.

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

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

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

      ליאור

      מחק