הייתה לי בעיה, בזמן שהשתמשתי ולימדתי פרקטיקות של אג'ייל כמו פיתוח מונחה בדיקות (TDD) בפרויקטים בסביבות שונות, שמתי לב לאותם אי הבנות ובלבולים.
מתכנתים רצו לדעת איך להתחיל, מה לבדוק ומה לא לבדוק, כמה צריך לבדוק בבת אחת, איך לקרוא לטסטים שלהם ואיך להבין למה טסט נופל.
ככל שהעמקתי בנושא ה-TDD יותר ויותר הרגשתי שהמסע שלי הפך להיות פחות מסע לכיוון שליטה הולכת וגוברת בנושא ויותר סדרה של מבואות ללא מוצא. אני זוכר מחשבות כמו "הלוואי ומישהו היה אומר לי את זה" לעיתים קרובות יותר מאשר מחשבות כמו "ואוו, דלת נפתחה בפניי". החלטתי שחייבת להיות דרך להציג TDD בדרך ישרה וטובה ושתמנע את הנפילות ואי ההבנות.
התגובה שלי הייתה – פיתוח מונחה התנהגות (BDD). זה התפתח מתוך פרקטיקות אג'יליות מסודרות ותוכנן כך שיהיה יותר נגיש ואפקטיבי לצוותים שפיתוח תוכנה אג'ילי היה חדש להם.
ככל שעבר הזמן, BDD התרחב והקיף תמונה רחבה יותר של ניתוח אג'ילי ובדיקות קבלה אוטומטיות.
שמות של מתודות חייבות להיות משפטים
ההארה הראשונה שלי הופיעה בזמן שהתוועדתי לתוכנית-עזר פשוטה לכאורה ששמה agiledox שנכתבה על ידי קולגה שלי בשם כריס סטיבנסון. התוכנית לוקחת test-class של JUNIT ומדפיסה את שמות הפונקציות בצורת משפטים קריאים. למשל test case הנראה כך:
public class CustomerLookupTest extends TestCase {
testFindsCustomerById() {
…
}
testFailsForDuplicateCustomers() {
…
}
…
}
יוצג באמצעות התוכנית בצורה כזאת:
CustomerLookup
– finds customer by id
– fails for duplicate customers
– …
המילה test הושמטה הן משם הclass והן משמות המתודות, כמו כן צורת האותיות הפכה מצורת camel-case (הכוונה לצורה שבה אות גדולה בראשית כל מילה, המתרגם) לטקסט רגיל. זה כל מה שזה עושה, אבל האפקט הוא מדהים.
מפתחים שגילו את זה יכלו לעשות לפחות מעין תיעוד עבורם, כך שהם התחילו לכתוב שמות מתודות שהיו למעשה משפטים אמתיים. יותר מזה, הם גילו שברגע שהם כתבו את שמות המתודות שלהם בשפה של אנשי ה – business, הם יצרו מסמכים שהיו מובנים למשתמשים, מנתחי מערכת ובודקים.
כתיבת משפטים פשוטים שומרת על מתודות הבדיקה ממוקדות
ואז במקרה התחלתי להשתמש בקונבנציה שבה מתחילים מתודת בדיקה במילה אמור (should). התבנית של: class זה אמור (should) לעשות משהו – משמעותה שאתה יכול להגדיר טסט רק לclass הספציפי הזה. זה שומר עליך ממוקד. אם אתה מוצא את עצמך כותב טסט שלא ניתן להגדיר את שמו באמצעות התבנית הזאת זה כנראה מעיד על כך שההתנהגות הצפויה שייכת למקום אחר.
למשל, כתבתי class שעושה ולידציה על קלט מהמסך. רוב השדות הינם פרטי משתמש רגילים – שם פרטי, שם משפחה וכדו', אבל אז היו שם גם שדות של תאריך לידה וגיל. התחלתי לכתוב את ה – ClientDetailsValidatorTest שהיו בו מתודות כמו למשל: testShouldFailForMissingSurname ו – testShouldFailForMissingTitle..
ואז התחלתי לחשב את הגיל ונכנסתי לעולם של חוקים לוגיים מציקים: מה אם התאריך לידה והגיל שהוזנו אינם תואמים? מה עושים כשהיום זה יום ההולדת? איך מחשבים גיל כשיש לי רק תאריך לידה? התחלתי לכתוב מתודות בדיקה מסורבלות בצורה הולכת וגדילה כדי לתאר התנהגות זאת. עד שהחלטתי להחליף את זה למשהו אחר. זה הוביל אותי לחשוב על class חדש שקראתי לו AgeCalculator שלו היה AgeCalculatorTest משל עצמו. כל התנהגויות חישובי הגיל הועברו לתוך המחשבון, כך שה – validator היה צריך רק טסט אחד לטובת חישוב הגיל לוודא שהוא פועל נכון עם המחשבון.
אם ה-class עושה יותר מדבר אחד, הייתי לוקח זאת בד"כ כאינדיקציה שאני צריך להוסיף עוד classes לעשות חלק מהעבודה. הגדרתי service חדש בתור interface שמתאר מה הוא עושה והעברתי את השירות כפרמטר ל – constructor של אותו class:
public class ClientDetailsValidator {
private final AgeCalculator ageCalc;
public ClientDetailsValidator(AgeCalculator ageCalc) {
this.ageCalc = ageCalc;
}
}
סגנון זה של שרשור אובייקטים יחד הידוע בתור: dependency injection הוא שימושי במיוחד בשימוש יחד עם mocks.
שם משמעותי של מתודת הבדיקה מאוד עוזר כשטסט נופל
לאחר זמן מה, מצאתי שכאשר אני משנה את הקוד וגורם לטסט להיכשל, יכולתי להסתכל על שם מתודת הבדיקה ולזהות מהי כוונת ההתנהגות של הקוד. בדרך כלל אחד משלושת הדברים הבאים היה קורה:
- גיליתי באג, "נו נו נו" לעצמי, הפתרון: לתקן את הבאג.
- ההתנהגות המכוונת הייתה עדיין רלבנטית אבל הועברה למקום אחר. הפתרון: להעביר את קוד הבדיקה ואולי לשנות אותו.
- ההתנהגות כבר לא הייתה נכונה. הנחת העבודה של המערכת השתנתה. הפתרון: מחק את הבדיקה.
האחרון יותר מצוי בפרויקטים אג'יליים ככל שההבנה שלך מתפתחת. לצערי, אנשי TDD טירונים פיתחו פחד מלמחוק בדיקות מתוך מחשבה שמשום מה זה מפחית את האיכות של הקוד שלהם.
היבט עדין יותר של המילה אמור (should) הופיע כשהשוויתי אותו עם אלטרנטיבות אחרות כמו: will או shall. Shouldבמרומז מאתגר אותך עם הנחת היסוד: האם זה אמור להיות כך (should it)? באמת? זה עושה זאת קל יותר להחליט אם הבדיקה נפלה בגלל באג שנמצא או פשוט בגלל שהנחות העבודה הקודמות שלך אודות התנהגות המערכת כבר אינם נכונות.
JBEHAVE מדגישה את ההתנהגות מעבר לבדיקות גרידא
בסוף 2003, החלטתי שהגיע הזמן לקיים "נאה דורש – נאה מקיים" התחלתי לכתוב תחליף לJUNIT שנקרא JBEHAVE, שהסיר כל התייחסות לבדיקות תוכנה והחליף אותו באוצר מילים שנבנה סביב הנושא של וידוא התנהגויות. עשיתי זאת כדי לראות איך framework מסוג זה יכול להתפתח אם אכווין אותו ישירות למנטרות החדשות של פיתוח-מונחה-התנהגות. גם חשבתי שהוא יכול להיות בעל ערך בתור כלי לימודי כדי ללמד TDD ו – BDD ובלי ההסחות של אוצר המילים המבוסס על בדיקות תוכנה.
כדי להגדיר את ההתנהגות של class היפותטי בשם CustomerLookup אני אכתוב behavior class ששמו לדוגמא יהיה CustomerLookupBehavior. הוא יכיל מספר מתודות שיתחילו במילה should. ה- behavior runner ייצור את האובייקט של ה – behavior class ויפעיל את כל ה – behavior methods לפי התור, כמו הדרך ש-JUNIT מבצעת במתודות הבדיקה שלה. בזמן הריצה יודפסו התוצאות לפי ההתקדמות ולבסוף יצא דו"ח סיכום. אבן הדרך הראשונה שלי הייתה להפוך את JBehave בעלת יכולת בדיקה עצמית. רק הוספתי את ההתנהגות שמאפשרת לרוץ בעצמה. הייתי יכול לבצע הגירה של כל הבדיקות של ה-JUNIT ל-תרחישי JBEHAVE ולקבל את אותו משוב מהיר כמו ב-JUNIT.
ההחלטה על ההתנהגות החשובה ביותר הבאה
אז גיליתי את הקונספט של הערך העסקי. כמובן כל הזמן הייתי מודע לכך שאני כותב תוכנה מסיבה מסוימת, אבל באמת מעולם לא באמת חשבתי על הערך של הקוד שאני כותב ברגע זה. קולגה נוספת שלי, המנתח העסקי כריס מייתס, גרם לי לחשוב על הערך העסקי בהקשר של פיתוח מונחה התנהגות.
בהינתן שהייתה לי מטרה מודעת להפוך את JBehve להיות self-hosting, גיליתי שהדרך המאוד שימושית להישאר ממוקד הייתה לשאול: מהו הדבר הבא החשוב ביותר שהמערכת איננה מבצעת?
השאלה הזאת מכריחה אותך לזהות את הערך של התכונות שטרם מימשת ולתעדף אותם. היא גם עוזרת לך לנסח את השם של ה- behavior method: המערכת לא מבצעת את X (כש-X הוא התנהגות כלשהיא בעלת משמעות), ו – X היא חשובה, מה שאומר שהמערכת אמורה לדעת לבצע את X; כך שה-behavior method הבאה שלך היא פשוט:
public
void
shouldDoX() {
// ...
}
עכשיו יש לי תשובה לעוד שאלת TDD, וזאת נקודת פתיחה להמשך.
דרישות הם גם התנהגויות
בנקודה זאת, היה לי Framework שעזרה לי להבין, ויותר חשוב מכך, להסביר, איך TDD עובד וגישה להימנע מכל המכשולים אותם חוויתי בעבר.
לקראת שנת 2004, בזמן שהייתי מתאר את המציאה שמצאתי, אוצר המילים של פיתוח מונחה התנהגות, לכריס מייתס, הוא אמר: "אבל זה בדיוק כמו אנליזה". הייתה הפסקה ארוכה שבה עיבדנו את זה, ואז החלטנו ליישם את כל החשיבה מונחת ההתנהגות להגדרת דרישות. אם נוכל לפתח אוצר מילים קונסיסטנטי למנתחי מערכת, בודקים, מפתחים, ואנשי ה- business, נוכל להיות בדרך למגר כמה מהאי-הבנות ודו-משמעויות ותקשורת לקויה שמתרחשת כאשר אנשים טכניים מדברים עם האנשים העסקיים.
BDD יוצרת שפה שימושית לאנליזה
בנקודה זאת, היה לי Framework שעזרה לי להבין, ויותר חשוב מכך, להסביר, איך TDD עובד וגישה להימנע מכל המכשולים אותם חוויתי בעבר.
לקראת שנת 2004, בזמן שהייתי מתאר את המציאה שמצאתי, אוצר המילים של פיתוח מונחה התנהגות, לכריס מייתס, הוא אמר: "אבל זה בדיוק כמו אנליזה". הייתה הפסקה ארוכה שבה עיבדנו את זה, ואז החלטנו ליישם את כל החשיבה מונחת ההתנהגות להגדרת דרישות. אם נוכל לפתח אוצר מילים קונסיסטנטי למנתחי מערכת, בודקים, מפתחים, ואנשי ה- business, נוכל להיות בדרך למגר כמה מהאי-הבנות ודו-משמעויות ותקשורת לקויה שמתרחשת כאשר אנשים טכניים מדברים עם האנשים העסקיים.
BDD יוצרת שפה שימושית לאנליזה
בערך באותו זמן, אריק איוונס פרסם את ספרו רב המכר Domain Driven Design, בו הוא תיאר את הקונספט של מידול מערכת באמצעות שפה שימושית מבוססת על הדומיין העסקי, כך שאוצר המילים העסקי יחלחל אל תוך קוד המערכת.
כריס ואני הבנו שאנחנו מנסים להגדיר שפה שימושית לתהליך האנליזה עצמו! הייתה לנו נקודת פתיחה טובה. באופן שכיח בתוך החברה שלנו כבר הייתה תבנית לסיפור שנראתה כך:
בתור [X]
אני רוצה [Y]
כך ש [Z]
כש- Y משמעותו פיצ'ר מסוים, Z הוא הרווח או הערך של אותו פיצ'ר, ו – X הוא האדם (או התפקיד) שירוויח מאותו פיצ'ר. העוצמה היא בכך שזה מכריח אותך לזהות את הערך של מימוש סיפור מיד כשאתה מגדיר אותו. ברגע שאין לסיפור שום ערך עסקי, לעיתים קרובות, יוצא משהו כזה:
"..אני מעוניין [ בפיצ'ר מסוים] כך ש [ אני פשוט מעוניין או.קי?]". זה הופך להיות קל יותר לסנן דרישות שהם יותר אזוטריות.
מנקודת התחלה זאת, מייתס ואני טיפלנו בנושאים שכל בודק ב-agile מכיר כבר: ההתנהגות של סיפור היא בסופו של דבר הקריטריון קבלה שלו – אם המערכת ממלאת את הקריטריון קבלה – ז"א שהיא מתנהגת בצורה תקינה; אם המערכת לא ממלאת את הקריטריון – היא איננה מתנהגת בצורה תקינה. אז יצרנו תבנית לתיחום ולכידה של קריטריון הקבלה של הסיפור.
התבנית הייתה צריכה להיות די קלילה כך שזה לא ירגיש מלאכותי או מאולץ לאנליסטים, עם זאת תבנית מובנית דיה כדי שנוכל לשבור את הסיפור למרכיביו ולמכן אותם. התחלנו לתאר את קריטריון הקבלה במונחים של תרחישים (scenarios) בצורה הבאה:
בהינתן (given) מצב התחלתי מסוים
בזמן (when) שאירוע מתרחש
אז (then) וודא התוצאות הנדרשות
להמחיש זאת, נשתמש בדוגמא הקלאסית של כספומט, אחד מהסיפורים עשוי להיכתב כך:
כותרת: לקוח מושך כסף
בתור לקוח
אני מעונין למשוך כסף מהכספומט
כך שלא אצטרך לחכות בתור בבנק
אז איך נדע שמימשנו סופית את הסיפור הזה? יש מספר תרחישים שיש להתייחס אליהם: החשבון עשוי להיות בעודף, או במשיכת יתר אבל בתוך המסגרת, החשבון עלול להיות במשיכת יתר מחוץ למסגרת. כמובן, יכולים להיות תרחישים נוספים, כמו למשל שהחשבון בעודף ומשיכת הכספים מכניסה אותו למשיכת יתר, או שאין מספיק מזומנים במכונה.
באמצעות התבנית של given-when-then שני התרחישים הראשונים עשויים להיכתב כך:
+תרחיש א: החשבון נמצא בעודף+
בהינתן (given) שהחשבון נמצא בעודף
והכרטיס תקף
והמכונה מכילה כסף
בזמן ש(given) הלקוח מבקש מזומנים
אז (then) וודא שהחשבון מחויב
והמזומנים נמשכו
והכרטיס הוחזר
שים לב לשימוש ב-"ו" החיבור (and) לחבר מספר תנאי פתיחה (given) או מספר תוצאות צפויות בדרך טבעית.
+תרחיש ב: החשבון נמצא במשיכת יתר מעל המסגרת+
בהינתן שהחשבון נמצא במשיכת יתר
והכרטיס תקף
בזמן שהלקוח מבקש מזומן
אז וודא שהודעת דחיה מופיעה
ושהכסף אינו נמשך
והכרטיס מוחזר
שני התרחישים מבוססים על אותו אירוע ואף חולקים מספר תנאי פתיחה ותוצאות משותפות. אנו רוצים לנצל זאת בכך שנבצע שימוש-חוזר לתנאי הפתיחה לאירועים ולתוצאות.
קריטריון הקבלה צריך להיות בר-ביצוע
המרכיבים של התרחיש – תנאי הפתיחה, האירוע והתוצאות – הינם מפורטים דים כדי להיות מיוצגים ישירות בקוד. JBehave מגדיר object model שמאפשר לנו למפות ישירות את מרכיבי הקוד ל- Jave Classes.
עליך לכתוב class שמייצג כל תנאי פתיחה:
public class AccountIsInCredit implements Given { public void setup(World world) { ... } } public class CardIsValid implements Given { public void setup(World world) { ... } } |
and one for the event:
public class CustomerRequestsCash implements Event { public void occurIn(World world) { ... } } |
וכן הלאה עבור התוצאות. JBehave משרשר את כולם יחד ומפעיל אותם. זה מייצר "עולם" שהוא איזה מקום לאחסן את האובייקטים שלך ואח"כ מעביר אותם לכל אחד מהתנאי פתיחה שלך בתורו כך שהם יוכלו לאכלס את העולם במצב נתון. JBehave מגלה לאירוע "להתרחש" בעולם, שמוציא לפועל את ההתנהגות הממשית של התרחיש. לבסוף הוא מעביר שליטה לאיזו תוצאה שהגדרנו לסיפור.
קיום class עבור כל רכיב מאפשר לנו לעשות שימוש חוזר ברכיבים בתרחישים שונים או בסיפורים שונים. בהתחלה הרכיבים מומשו באמצעות mocks לאתחל חשבון בעודף או כרטיס תקף. זה מה שמייצר את נקודות הפתיחה למימוש התנהגות. עם מימוש האפליקציה, תנאי הפתיחה והתוצאות מתעדכנים בclasses האמתיים שמומשו, כך שבזמן שהתרחיש בוצע הם הופכים להיות בדיקות E2E נאותות.
ההווה והעתיד של BDD
לאחר הפסקה קצרה, JBehave חזרה להיות בפיתוח. הליבה די מושלמת ורובוסטית. הצעד הבא הוא אינטגרציה עם IDE פופולריים של Java כמו IntelliJ IDEA ו – Eclipse.
דייב אסטלס מקדם בצורה אקטיבית את BDD, הבלוג ומאמרים שונים שפרסם עוררו פרץ של פעילות. בצורה בולטת במיוחד – פרויקט ה-rspec למימוש framework של BDD בשפת רובי. אני התחלתי עבודה על rbehave שתהיה מימוש של JBehave בשפת רובי.
מספר שותפים שלי משתמשים בטכניקות BDD במגוון של פרויקטים בעולם האמתי ומצאו את הטכניקות הללו מאוד מוצלחות. ה-JBehave story runner – החלק שאחראי על אימות קריטריון הקבלה – נמצא בפיתוח אקטיבי.
החזון הוא שיהיה עורך שבו מנתחי מערכת ובודקים יוכלו ללכוד סיפורים בעורך טקסט רגיל שיוכל לחולל stubs לטובת ה- behavior class כל זה בשפה של ה – business domain. BDD התפתח בעזרתם של הרבה אנשים ואני אסיר תודה לכולם.