TDD – תכנות מונחה מבחנים (Part Two)

10 באוגוסט 2007

אין תגובות


בפוסט הראשון בסדרה, הכרנו את המוטיבציה מאחורי TDD, ועכשיו, הגיע הזמן להתחיל להרגיש את זה. כדי להציג את הנושא, בחרתי לתכנת דווקא משחק פשוט, שנקרא Craps. זהו משחק הימורים נחמד, עם מערכת חוקים מסובכת, אבל עם רעיון מאוד פשוט – אחד השחקנים זורק קוביות, ולפי התוצאות של הקוביות שלו, אפשר לדעת אם הימור זכה או הפסיד.


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


שלב ראשון – הורדה, התקנה, הכנה לשימוש, והתפעלות ראשונית.


בחלק הראשון של הסדרה, אמרתי שאני הולך להשתמש ב- NUnit בתור תשתית הבדיקות, ואפשר להוריד אותה מכאן, אם אתם מתכננים לעקוב אחרי הכל.


יש מספר דרכים לעבוד עם NUnit. בין השאר, יש לה גירסת Command Line, וגירסה שיושבת יפה עם Visual Studio, אבל בגלל שיש כזה GUI מגניב, שווה להתרכז דווקא בו. לאחר ההתקנה, תוכלו להריץ את ה- NUnit GUI מתפריט "Start", ותקבלו את החלון הבא:


image


זהו החלון המרכזי של התוכנה, וכרגע, אין יותר מדיי מה לעשות איתו.


גם ה- Visual Studio שלי פתוח, עם שני פרוייקטים חדשים לגמריי: הראשון, נקרא Craps, השני – CrapsTests. לא כתבתי שורת קוד אחת, ואני עושה Build. זמן לחזור אל ה- NUnit, ולגרום לו להכיר את הפרוייקט שלנו : באמצעות File-Open, אני פותח את את הקובץ CrapsTests.dll (שנמצא תחת הפרוייקט ב- CrapsTest\Bin\Debug) – ומתקבלת ההודעה הבאה:


image


לא מדאיג. בואו נמשיך. ב- Visual Studio, נוסיף Reference לפרוייקט של המבחנים ל- NUnit.Framework (בלשונית .net) ול- Craps (בלשונית Projects).


נשנה את הקובץ היחיד שבפרוייקט הזה, כך שייקרא CrapsTableTests (תודה לאל ששינוי שם קובץ ב- Visual Studio מציע לשנות גם את שם ה- Class), ואת ה- Class 'נקשט' ב- Attribute שנקרא TestFixture כדי להודיע ל- NUnit שכאן יש מבחנים שאנחנו רוצים להריץ. (על הדרך, נוסיף לקוד שלנו את הכרזת  ה- Using המתאימה). בסיום התהליך, הקוד נראה כך:



    1 using System;


    2 using System.Collections.Generic;


    3 using System.Text;


    4 using NUnit.Framework;


    5 


    6 namespace CrapsTests


    7 {


    8     [TestFixture]


    9     public class CrapsTableTests


   10     {


   11     }


   12 }


 נשים לב שבחלון של NUnit, משהו השתנה – פתאום העץ תחת לשונית Tests מכיר ב- Class הזה שלנו. הגיע הזמן להתחיל לתכנת.


שלב שני – מבחן ראשון


ב- NUnit, מבחן הוא שיטה ציבורית (מתודה שהיא Public) מסוג void, שלא מקבלת פרמטרים, שמקושטת ב- Attribute בשם Test. המבחן הראשון שאני חושב לכתוב ייקרא CanCreateCrapsTable – ויש לו חשיבות מיוחדת – כאן חושבים קצת על איך ליצור את ה- Class שלנו (Instantiate, זה המינוח המדוייק). בואו נעשה את זה פשוט:



        [Test]


        public void CanCreateCrapsTable()


        {


            CrapsTable ct = new CrapsTable();


            Assert.IsNotNull(ct);


        }


השורה השנייה, זו שמכילה את הוראת Assert.IsNotNull, שווה הסבר מפורט. הוראת Assert אומרת ל- NUnit שיש משהו שרוצים לבחון – במקרה הזה, לבחון שהאובייקט שמוחזר אינו Null. ההיגיון בכך הוא פשוט – אם האובייקט יוחזר Null, המבחן ייכשל, אם האובייקט יוחזר לא Null, המבחן יצליח – והוראת ה- Assert היא זו שתודיע ל- NUnit.


אספר לכם שאצלי מודלקת אפשרות ב- NUnit (בחלון Options, בלשונית Test Load) שנקראת Reload when test assembly changes ו- re-run last tests run, כך שכל פעם שאני לוחץ על F6 (שזה Build, אם אתם משתמשים בקונפיגורציה של C# ב- Visual Studio), ה- NUnit GUI מריץ את המבחנים בעצמו, ומדווח.


image


נחזור לקוד שלנו. מכאן, ננסה לגרום לזה להתקמפל. ניצור מחלקה בפרוייקט Crops בשם CropsTable (למען האמת, פשוט שיניתי את השם של Class1.cs, ונתתי ל- VS לעבוד בשבילי). לאחר קימפול, נקבל בחלון של ה- NUnit פס ירוק, שאומר שהמבחן הצליח:


image 


הוראות Assert מגיעות במספר טעמים: מ- "IsNotNull" עד "GreaterThen", והן מאפשרות לכתוב מבחנים מגוונים, וממוקדים כאחד. כדאי במיוחד לשים לב שגם קיימת הוראת Assert.Fail.


כשהמבחן מצליח, זה טוב, לא?


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


זה פחות חשוב למבחן הראשון, לדעתי. במבחנים הבאים נראה את זה קורה הרבה קודם.


מבחן שני – Rules Of Play מתחיל כאן


Craps הוא משחק לשחקן אחד או יותר. יש שחקן שנקרא Shooter, שכאשר מתחיל הסיבוב, הוא זורק את הקוביות. נוסיף אם כן, Class שייצג את השחקן. נתחיל מהמבחן:



        [Test]


        public void CanAddPlayer()


        {


            CrapsTable ct = new CrapsTable();


            Assert.IsNotNull(ct);


 


            CrapsPlayer player = new CrapsPlayer("Joe");


            Assert.AreEqual(0, ct.Players.Count);


 


            ct.AddPlayer(player);


            Assert.AreEqual(1, ct.Players.Count);


        }


הרשתי לעצמי לבצע את הפעולות הדרושות לכך שזה יתקמפל – יצרתי את CrapsPlayer בקובץ משלה, עם Constructor שמקבל את השם של השחקן, ושומר אותו במשתנה מקומי בשם m_name.


המחלקה הזו נראית כך:



    public class CrapsPlayer


    {


        private string m_name;


        public string Name { get { return m_name; } }


 


        public CrapsPlayer(string Name)


        {


            m_name = Name;


        }


    }


שום דבר מסובך, נכון ?


בנוסף, השמשתי ב- Generate Method של Visual Studio בכדי ליצור את המתודה AddPlayer. ניתן לעשות את זה מהמקלדת, באמצעות שילוב הכפתורים Ctrl ונקודה (תודה לגיא! שינית לי את החיים). כדי שזה יתמפל, הוספתי את השורה הבאה ל- CrapsTable:



public List<CrapsPlayer> Players;


זה מאפשר לקוד להתקמפל, וה- NUnit צועק:


image


ובצדק – חוץ מלהגדיר List, לא עשינו כלום.


נוסיף Constructor ל- CrapsTable, שיאתחל את הרשימה הזו ל- List פשוט:



public CrapsTable()


{


    Players = new List<CrapsPlayer>();


}


כעת, אחרי Build, הקוד קורס אחרת. קודם, NUnit צעקה ש- Players אינו מאותחל (למען האמת, היא צעקה על NullReferenceException), ועכשיו היא צועקת על כך ש- "The method or operation is not implemented". נפתור את זה, ע"י שנתקן את הקוד של AddPlayer.



        public void AddPlayer(CrapsPlayer player)


        {


            //throw new


            //Exception("The method or operation …");


            Players.Add(player);


        }


השארתי את הקוד 'הישן' כהערה, כדי שתראו על מה NUnit צעקה.


עכשיו, NUnit שמחה: מופיע שוב שהכל ירוק, וזה נהדר.


שלב אחרון להיום – ניקוי


חלון ה- NUnit נראה כעת כך:


image


ובשלב זה, נרצה לעשות שינויים בקוד, שישמרו על המצב הירוק הזה. זהו שלב ה- Refactoring. השינוי הראשון שאני רוצה לעשות הוא להחביא את ה-List המסויים מאחורי Interface. אז נשנה את השורה שמגדירה את הרשימה.




public IList<CrapsPlayer> Players


הדבר השני, יהיה להחביא את הרשימה מאחורי Property, כך :


 



        private IList<CrapsPlayer> m_players;


        public IList<CrapsPlayer> Players


        {


            get { return m_players; }


        }


נעשה Build, כדי לוודא ששום דבר לא התקלקל בינתיים – ואכן, המבחנים עוברים (שנאמר, Green Bar).


האם יש עוד משהו שכדאי לנקות ? הקוד של הפרוייקט Craps הוא יפה, נראה טוב (עדיין חסר תעוד, אבל זה להמשך), אבל משהו בקוד של המבחן מתחיל להעלות ניחוחות של Code Duplication: במבחן השני (CanAddPlayer השתמשנו בכל השורות של המבחן הקודם, CanCreateCrapsTable, רק בשביל באמת ליצור את השולחן. נאה יהיה אם נעביר את זה למתודה פרטית, בקוד של המבחן, כך :



        [Test]


        public void CanCreateCrapsTable()


        {


            CrapsTable ct;


            ct = CreateCrapsTable();


            Assert.IsNotNull(ct);


        }


 


        private static CrapsTable CreateCrapsTable()


        {


            CrapsTable ct;


            ct = new CrapsTable();


            Assert.IsNotNull(ct);


            return ct;


        }


 


על פניו, אנחנו מבצעים את אותה הבדיקה פעמיים, וזה נכון. הבדיקה הראשונה, היא החשובה, והיא מתרחשת בתוך המתודה שיוצרת את השולחן. כך, כל המתודות שצריכות ליצור שולחן ישתמשו באותו קוד, והוא קוד שבודק את עצמו. למה לשים אז קריאה זהה ב- CanCreateCrapsTable ? זה משפר את קריאות המבחן (Test Readability), ומהווה רשת ביטחון למקרה שמשהו במתודה CreateCrapsTable יתפקשש.


סיכום הפרק


בפרק זה ראינו מאיפה מורידים ואיך מתקינים את NUnit, איך כותבים מבחנים עבור NUnit (מבחן ראשון), וראינו בנגיעה איך כתיבת המבחנים משפיעה על הקוד שלנו (מבחן שני). ראינו גם את המעגל (והמנטרה) של "Red-Green-Refactor", והכי חשוב – כתבנו את הבסיס הראשוני למשחק ה- Craps שלנו.


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



  • הקוד שנכתב, נכתב כאשר אנחנו כבר יודעים איך אנחנו רוצים להשתמש בקוד. כך, יש לנו דרך ברורה לדעת מתי להמשיך קדימה, ומעגל הפיתוח יציב יותר.
  • לא נכתבה אף שורת קוד מיותרת – כל שורת קוד יש בה טעם, ויש לה יעוד, שהוא ברור מקריאת הקוד.
  • יש מבחנים לקוד. כך, אין צורך לכתוב Unit Test אחרי כתיבת הקוד, כי הוא כבר כתוב. בעתיד, אם נבצע שינוי שידפוק משהו קיים, נדע על כך בצורה מיידית.
  • ניתן לסכם את כל אלא כך: הצעדים, שבהם הקוד מתקדם, הם קטנים מאוד, ומהירים מאוד. וזה הקסם האמיתי.

המנטרה, כמו שכבר צויין, היא "Red-Green-Refactor", כמו שראינו במבחן השני. בסופו של דבר, היינו יכולים להשתמש בטכניקה הזו גם ביצירה של המתודה CreateCrapsTable, שתחזיר קודם כל Null, ורק אז לתקן אותה, אבל זה שקר – המתודה הזו נוצרה בשלב ה- Refactoring, ולא היה לי דרך לדעת עליה לפני השלב הזה (עוד נקודה לזכות TDD…).


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


לאן מכאן


בדרך כלל, השלב הבא אחרי שכותבים את המבחנים הראשונים, הוא הצגה של Mock Objects. אני לא מאמין בזה. אני חושב שכדאי לעבור קודם על כמה Design Patterns & Practices, כמו Dependency Injection, מפעלים, Observerים ו- Commands, שימוש והטמעה של MVC, וכל מיני כאלה. למרות זאת, השלב הבא יהיה כתיבה של עוד מבחנים, במטרה לסיים את הקוד של Craps.


חבר שלי, אחרי שקרא את החלק הראשון, שאל אותי 'ומתי זה יתחיל להיות מגניב'. התשובה שלי היא פשוטה: אני מקווה שבסביבות הפוסט ה-20 בסדרה, ממשק המשתמש ימומש ב- WPF, שיתקשר עם WCF לשרת. אני גם חושב שיש היגיון בלהגדיר את כל התנהלות השולחן באמצעות WF, אבל אלה מחשבות לעתיד, ובינתיים, לא נותר לי אלא לאחל לכם לילה טוב – ולבקש שתגיבו על המאמר. ההערות שלכם עוזרות לכתוב את הפרק הבא :)…

הוסף תגובה
facebook linkedin twitter email

כתיבת תגובה

האימייל לא יוצג באתר. שדות החובה מסומנים *