אצלי אין באגים - חלק 2
אתמול הדבקתי כאן קטע קוד קצר שכולל באג וראינו שכשהקוד קצר הרבה פעמים גם הבדיקה קצרה ולכן שווה להשקיע עוד עשר דקות ולכתוב אותה. אני רוצה היום להמשיך את הדיון הזה עוד צעד אחד ולשאול - איזה בדיקות לכתוב? האם צריך יותר מבדיקה אחת? והאם בדיקות נוספות על אותו הקוד יכולות ללמד אותי משהו חדש או להגן עליי מבעיות אחרות?
כתזכורת זאת הפונקציה ברובי איתה היתה בעיה:
def perform(workshop_instance)
return if (workshop_instance.date - Time.zone.now) > 1.hour
return if workshop_instance.passed?
zoom = Zoom.new
emails = zoom.meeting_registrants(workshop_instance.zoom_id)
NotificationsMailer.webinar_reminder(workshop_instance, emails).deliver_now
end
שני סוגי הבדיקות המרכזיים שנרצה לכתוב הם Acceptance tests ו Unit tests. בדיקות מסוג Acceptance Tests יאפשרו לנו להיות רגועים ולדעת ששעה לפני הוובינר כשהג'וב הזה ירוץ הוא באמת ישלח את המייל. בדיקות מסוג Unit tests יעזרו לנו למצוא באגים בקוד אם אנחנו חושדים שתרחישים מסוימים הולכים להתפוצץ.
כדי להחליט איזה Acceptance Tests אנחנו צריכים נסתכל על הקוד כחלק מתהליך עסקי ונרצה לשאול: מה המטרה של הקוד הזה? מה הקלט שהוא מקבל? באיזה מצבים הוא אמור לרוץ? ומה אמור לקרות בכל אחד מהמצבים? הנה הרשימה שלי:
הג'וב עשוי לרוץ במצב שהוובינר רחוק. זה יקרה אם מישהו ישנה את השעה של הוובינר אחרי ששלחתי את הג'וב לתור ואז כשהג'וב יתעורר הוא יגלה שבעצם הוא כבר לא רלוונטי. במצב כזה צריך פשוט לא לעשות כלום. והנה בדיקת הקבלה הראשונה שלנו: יוצרים וובינר שיקרה מחר ומריצים את הג'וב. אם הכל ילך טוב לא נמצא מייל תזכורת בתיבה.
הג'וב עשוי לרוץ אחרי שהוובינר כבר התקיים. במצב כזה אנחנו לא רוצים לשלוח תזכורת ולכן נוכל ליצור וובינר בעבר ואז להפעיל את הג'וב. התוצאה הצפויה שוב תיבת אימייל ריקה.
הג'וב עשוי לרוץ בדיוק בזמן, ובמצב כזה נרצה למצוא את מייל ההזמנה בתיבה. בשביל להפוך את הבדיקה לכמה שיותר ריאליסטית כדאי ליצור וובינר, לרשום אליו מספר אנשים (חלקם עם כתובת מייל לא תקנית) ואז להריץ את הג'וב. התוצאה הצפויה היא מייל תזכורת בכל אחת מתיבות המייל התקניות שנרשמו לוובינר.
הבחירה כמה Acceptance Tests לכתוב וכמה להתאמץ כדי לדמות את המציאות בצורה כמה שיותר מדויקת היא המשחק שלכם בכתיבת הטסט. יש מצבים שאנחנו רוצים בדיקות כמה שיותר ריאליסטיות וכמה שיותר מדויקות, ויש מצבים שאנחנו מוכנים להסתפק בפחות בשביל שיהיה יותר קל לכתוב את הטסט או יותר מהר להריץ אותו. בריילס למשל יש כבר מנגנון של Mock לתיבות אימייל ולכן כמעט אף פעם אני לא מגדיר תיבת אימייל אמיתית ובודק שמייל באמת נשלח אליה. מצד שני בעבודה עם APIs חיצוניים (כמו זום במקרה של הקוד הזה) כמעט תמיד אעדיף להשתמש ב API האמיתי בתוך הטסט כדי לוודא שאני תופס שינויי API מצד הספק.
הרשימה השניה של בדיקות היא בדיקות היחידה וזו רשימה שהמטרה שלה אחרת לגמרי. בעוד שבדיקות Acceptance נכתבו במטרה לוודא שהקוד אכן מקיים את ההבטחה שלו, בבדיקות יחידה המטרה היחידה היא לחסוך זמן Debugging, כלומר לכתוב מנגנונים שיכוונו אותי למקום בו יש באג.
בשביל להחליט על בדיקות היחידה אני מסתכל על הקוד שורה אחר שורה ומנסה לזהות מה יכול להשתבש בכל שורה. כל פעם שזיהיתי שמשהו רע יכול לקרות אני יכול ליצור ממנו בדיקה שתעזור לי לזהות שהתקלה באמת נגרמה בגלל אותה שורה. כבר במבט זריז על הקוד אנחנו רואים שכתיבת בדיקות יחידה לקוד תחייב אותנו קודם לעשות Refactoring, כי הפונקציה "עושה" יותר מדי. הנה ניתוח שורה אחר שורה של הקוד והבעיות בו:
שורה ראשונה:
return if (workshop_instance.date - Time.zone.now) > 1.hour
הייתי שמח לבדוק שהתנאי אכן מחזיר "אמת" אם הוובינר נקבע למועד רחוק מדי בעתיד. אבל מבנה הפונקציה הנוכחי לא מאפשר לי לעשות זאת. בשביל לבדוק את המנגנון כדאי להוציא את התנאי לפונקציה נפרדת על אוביקט workshop_instance
, כלומר להחליף את השורה בשורה בסגנון הזה:
return if workshop_instance.time_to_start > 1.hour
עכשיו אני יכול לכתוב בדיקות על WorkshopInstance.time_to_start
כדי לראות שהוא מחזיר את הערך הנכון, ובנוסף לדרוס את הפונקציה ולהחליף אותה ב Mock Object, ובו ליצור פונקציית "גדול מ..." מזויפת, ואז לוודא שאותה פונקציית "גדול מ..." נקראת עם פרמטר של שעה אחת.
שורה שניה:
return if workshop_instance.passed?
שורה זו כבר כתובה יותר טוב - היא מוודאת שהוובינר לא בעבר באמצעות שימוש בפונקציה passed?
של WorkshopInstance. בשביל לזהות שיש באג בוובינר שתוכנן לעבר מספיק לי לבדוק שהפונקציה passed?
מצליחה לזהות את ההבדל בין וובינרים שנקבעו לעבר לוובינרים בעתיד.
שורה שלישית:
zoom = Zoom.new
השורה לא עושה יותר מדי ולכן אין כאן מה לבדוק. אם הפונקציה Zoom.new
היתה צריכה לקבל פרמטרים מסוימים ייתכן והייתי רוצה ליצור בדיקה שמגדירה Mock Object במקום המחלקה Zoom
ושמוודאת שקוראים לפונקציה new
עם הפרמטרים המתאימים.
שורה רביעית:
emails = zoom.meeting_registrants(workshop_instance.zoom_id)
באג בשורה הזאת אומר שקיבלתי את רשימת האימיילים הלא נכונה, או ששלחתי מזהה פגישה לא נכון. בשביל לזהות כל אחת משתי הבעיות הפוטנציאליות האלה אני צריך לכתוב שתי בדיקות: הבדיקה הראשונה דורסת את הפונקציה meeting_registrants
של zoom ומחליפה אותה ב Mock Object שמוודא שהפונקציה נקראה עם הפרמטר הנכון.
בשביל הבדיקה השניה נתבונן בשורה הבאה:
NotificationsMailer.webinar_reminder(workshop_instance, emails).deliver_now
כאן אנחנו לוקחים את המשתנה emails שיצרנו קודם ומעבירים אותו ל NotificationsMailer. לכן אפשר לדרוס את NotificationsMailer.webinar_reminder
ולזהות שהפונקציה נקראת עם אוביקט הוובינר ובפרמטר השני עם רשימת האימיילים הנכונה.
סך הכל בדיקות היחידה שנרצה לכתוב הן:
לוודא שבדקו אם
WorkshopInstance.time_to_start
גדול משעה.לוודא שבדקו אם
WorkshopInstance.passed?
.לוודא שהפעילו את
Zoom.meeting_registrants
עם מזהה הפגישה הנכון.ליצור
Zoom.meeting_registrants
פיקטיבי שמחזיר רשימה של פרטי נרשמים מזויפים כמו שזום היה עשוי להחזיר, ולוודא שהפונקציהNotificationsMailer.webinar_reminder
מקבלת את רשימת האימיילים של הנרשמים.
אם היינו כותבים את כל הבדיקות האלה היינו מגלים את הדברים הבאים:
בדיקת הקבלה השלישית היתה נכשלת. אחרי הפעלתה מייל התזכורת לא הופיע בתיבה.
בדיקת היחידה הרביעית היתה נכשלת. הפונקציה
NotificationsMailer.webinar_reminder
לא הופעלה עם רשימת כתובות המייל אלא עם רשימת כל פרטי הנרשמים (כלומר רשימה של אוביקטי מידע שכוללת גם את כתובת המייל אבל גם את השם, הכתובת, הטלפון וכל הפרטים האחרים שזום היה מחזיר לגבי הנרשמים).
כישלון בדיקת הקבלה היה שולח אותי לשבור את הראש באיזה חלק מהפונקציה היה הבאג. כישלון בדיקת היחידה היה מזהה בצורה מיידית שהבעיה בשורות הרביעית והחמישית: שכחתי לגזור מרשימת הנרשמים לוובינר את שדה כתובת המייל, ובטעות העברתי את הרשימה המלאה עם כל הפרטים לפונקציית שליחת האימייל.
בעולם האמיתי אני חושב שכתיבת בדיקות קבלה היא קריטית להצלחה של מערכת ולשקט הנפשי שלנו כמתכנתים. כתיבת בדיקות יחידה יכולה לעזור למצוא באגים אבל צריך להיות שקולים בעניין ולא לכתוב אותן "על עיוור" רק בשביל להגיע לכיסוי קוד טוב. תשבו עם עצמכם ונסו לזהות כמה זמן לוקח לכם למצוא את הבעיה אחרי שאתם יודעים שיש בעיה, ותכתבו בדיקות יחידה במקומות שאתם באמת מבזבזים הרבה זמן על למצוא אותה.