2. Overview Test Driven Development Overview The motivations for and challenges of testing our production code Techniques + a real life example
3. Unit Testing Exercise classes in an isolated environment Verify that actions on the code produce predictable, consistent results Write Tests Write Code Pass All Tests Refactor
4. The price of Unit Testing New logic is more time consuming to write Old logic may require extensive Refactoring to be testable Amount of code to maintain increases Illusion of being bug free when tests pass No Stage in FlexUnit The actual visual output can’t be fully tested
5. The benefits of Unit Testing Better definition and understanding of the code’s purpose Helps improve code design Empowers refactoring Documents the usage of the code A small, quick environment to debug in Provides immediate feedback when the code changes (Adding/modifying functionality, Refactoring, Merging)
6. TDD ROI conclusions The price: Initial additional development time The benefit: Cumulative reduction in defects The conclusion: No brainer for long term projects
7. The Challenges Classes and functions have multiple responsibilities Classes are strongly coupled to other classes Some of these couplings are to Singletons Ambiguous dependencies (Objects, callbacks) In short, code changes have unknown side effects that are hard to foresee
8. The Motivation Our production code is the result of several years of rat chase Rapid Development Refactoring core code is dangerous Augmenting core capabilities is expensive Reducing code volatility may save us from a dangerous rewrite, or allow us to reuse code when a rewrite occurs Moving to CI is key to remaining competitive
9. Legacy Testing Heuristic Before making the change, write passing tests for the area around your change These tests are intended to preserve the behavior of the code prior to your change Add failing tests that define the change you’re making Apply your change and make all tests pass
17. Safe Modification: Function extraction Preserve the original functions’ signature. Extract the function to its own class Call the new function with the same signature, and a reference to an interface representing the original class
19. Safe Modification: Function extraction Preserve the original functions’ signature. Extract the function to its own class Call the new function with the same signature, and a reference to an interface representing the original class Use the IDE to determine what calls are to be redirected to the original class
21. Safe Modification: Function extraction Preserve the original functions’ signature. Extract the function to its own class Call the new function with the same signature, and a reference to an interface representing the original class Use the IDE to determine what calls are to be redirected to the original class Put those calls in the interface
24. Safe Modification: Function extraction Preserve the original functions’ signature. Extract the function to its own class Call the new function with the same signature, and a reference to an interface representing the original class Use the IDE to determine what calls are to be redirected to the original class Put those calls in the interface Create a mock class implementing the interface for testing
27. Breaking encapsulation on purpose Encapsulation is a tool that makes the code usage easier to understand The need to break it often points to problem in the design of the code usage Problems in design can’t be solved without Unit Tests in place The break can help in understanding what needs to be changed Damage can be minimized with self documenting code
29. Solving Singletons dependency You can’t use a testing instance Create a testing only function to set a testing instance
30. Solving Static dependencies You can’t subclass or mock a static class Replace reference to static variables with getters and static functions to local functions Subclass and override with a test class to use your own data/logic In case of a massive amount of static calls, use the same class name in your testing project to override the original class, and delegate all calls to another class
31. Conclusion Getting legacy code into a testing environment is possible Most of the benefits of TDD can be achieved with partial coverage Adapting TDD during any stage of the development is worthwhile. The bigger the project, the more beneficial it is
אני אעבור מאוד בקצרה על יוניט טסטינג, יתרונות, חסרונות, אח"כ אני אספר קצת על הרקע להחלטה של שינוי מתודולוגיית הפיתוח, וההשלכות המקצועיות של ההחלטה הזו, ולבסוף אני אעבור על טכניקות פיתוח של הכנסת קוד שעל פניו נראה בלתי אפשרי לבדיקה, לתוך סביבת בדיקה בצורה בטוחה, בלי לשנות את ההתנהגות שלו בתהליך, עם דוגמה אמיתית ממצולות הקוד בייס של וויקס.
אז קודם כל קצת על יוניט טסטינג, האבן יסוד של TDD. על רגל אחת, מדובר על קוד שמריץ קוד ובודק את התוצאות בהנתן כל מני אינפוטים.בתחילת כל טסט, אינסנטס חדש של הקלאס נוצר, ופעולה אחת בלבד מתבצעת – בד"כ קיראה לפונקציה עם ערך כלשהו. לבסוף, בודקים שהפעולה גרמה לתוצאה הצפויה. אם כן, הטסט עובר. אם לא, הטסט נכשל.הסייקל פיתוח ב TDDהוא פחות או יותר ככה: מפתח כותב פעולה אטומית, שיכולה להיות פיצ'ר, חלק מפיצ'ר, תיקון באג, בד"כ לא יותר מכמה שורות קוד, ובמבקביל כותב טסטים או מתקן טסטים כדי לבדוק שהקוד שהוא הרגע כתב עושה מה שהוא אמור לעשות. בגישה הקלאסית אומרים תמיד לכתוב הטסט קודם ואז את הקוד הפשוט ביותר שגורם לטסט הזה לעבור, אבל אני לא דוגמטי לגבי הסדר, וכך גם רוב מפתחי ה TDD שדיברתי איתם.אחרי שהפיצ'ר כתוב וכל הטסטים כתובים ועוברים, אפשר לשכתב את הקוד כך שיהיה יותר יפה או יותר יעיל. כיוון שיש לנו טסטים שמוודאים את תקינות הקוד, אנחנו חופשיים לשכתב בסייקלים מאוד מהירים ובלי המון מחשבה על שינוי פוקציונליות, אם עשינו טעות, לפחות טסט אחד אמור להכשל, ומייד יש לנו פידבק שעשינו טעות.ולבסוף, כמובן חוזרים וממשיכים בכתיבה של פעולה אטומית חדשה.
אני לא אעבור על כל הנקודות, רק אציין שבגדול אפשר לומר שזמן הפיתוח מתארך כשעובדים עם טסטים. מן הסתם, מדובר בעוד קוד שצריך לכתוב.יוניט טסטים גם לא נועדו לבדוק ממשק או תוצאות ויזואליות. לדוגמא, המנגנון צביעה של וויקס נבדק ע"י טסטים בכל שלב בתהליך, חוץ מהאם באמת בסוף אובייקטים נצבעו כמו שהם אמורים להצבע.
כל הנקודות האלה מסתכמות פחות או יותר בכך שלאורך זמן, כמות הבאגים שהמוצר צובר פחותה בהרבה ביחס לפיתוח ללא טסטינג. זוהי נקודה שנבדקה ע"י מספר מחקרים ונמצאה כנכונה.
אז החסרון העיקרי של יוניט טסטינג זה זמן פיתוח ראשוני ארוך יותר, והיתרון העיקרי של יוניט טסטינג זה ירידה משמעותית בהצטברות באגים. אז לפרוייקטים ארוכי טווח, עבודה עם יוניט טסטים צריכה להיות החלטה יד מובנת מאליה.י
עם קצת נסיון ביוניט טסטינג, בדיקה של קוד חדש היא עניין של מה בכך. האתגר הטכני הוא, כיצד בודקים קוד ישן שצריך לבצע בו שינויים? קלאסים שלא נכתבו במתודולוגיה של טסטים נוטים להיות מאוד תלותיים בקלאסים אחרים, פשוט כי יותר טבעי לכתוב עם תלות מאשר בלי. הנה דוגמה מהקוד בייס שלנו. לאחרונה הכנסנו את האדיטור שלנו לתוך מעטפת פלקס, מתוך כוונה להמיר לאט לאט את ה UI שלנו לפלקס. יש לנו דיאלוג להוספת אייטמים, שמשתמשים בה בהרבה מקומות במוצר, ובין השאר מתוך דיאלוג שהומר לפלקס. כדי לפתור כל מני בעיות, היינו צריכים שהדיאלוג הוספה הזה, כשהוא נפתח מהדיאלוג הפלקסי, ייפתח בתוך מעטפת פלקסית גם הוא. הקוד שפותח את הדיאלוג הוספה נמצא בתוך קלאס מאוד ישן ומאוד גדול, שנקרא EdWizardManager
אנחנו בוויקס עובדים על פרוייקט ארוך טווח, ולא אימצנו טסטינג מתחילת הדרך. בתחילת קיומה, כמו כל סטארט אפ, וויקס היתה במירוץ כנגד השעון, להגיע למוצר מוכר לפני שכסף ההשקעה זולג או שמתחרים משתלטים על השוק עם מוצר מתחרה. הפיתוח המאוד מהיר הוכיח את עצמו בסופו של דבר, כשוויקס הצליחה לצאת מנצחת עם מוצר מוביל בשוק, אבל הקוד בייס של המוצר הגיע למצב שהיה ריסקי לבצע בו שינויים. מצד אחד המון לקוחות תלויים ביציבות שלו, ומצד שני הוא לא כתוב בצורה שנותנת פידבק על תוצאה של שינויים. גרסאות התחילו להתארך ולהסתבך, הQA נהיה תהליך יקר, והבנו שכדי להתקדם בצורה משמעותית מעתה והלאה, צריך לשנות את תפיסת הפיתוח.בהתחלה הוחלט על שכתוב. נשכרו אנשים והוקם צוות שהתחיל לעבוד על וויקס 2, אבל באותה תקופה גם התחילה הפניקה בתעשייה מהמהלך המוצהר של אפל כנגד פלאש. וויקס לא רצתה להשאר מחוץ לשוק ההולך וגדל של האייפונים, והוחלט לנטוש את עבודת השכתוב, לטובת כתיבת מוצר חדש שנותן שירות בניית אתרים דומה לוויקס בפלאש, אבל ב HTML. למרות שבאותה תקופה האמנתי בשכתוב, וגם רציתי להיות מעורב בשכתוב מתוך עניין מקצועי, בדיעבד אני מאוד שמח שנמנענו מזה. הסיכון והעלויות של שכתוב מוצר הם עצומים, ולאחר ששינינו את תפיסת הפיתוח על המוצר הקיים, הסתבר גם שהרבה מהבעיות של הקוד בייס המקורי הן פתירות. אז שינינו את תפיסת הפיתוח. הוחלט לאמץ בכל מחלקות הR&D חזון של CI, קוניטינוס אינטגריישן. בשביל הפלאש קליינט, שנכתב במתודולוגיה הפוכה לחלוטין לזו של CI, היינו די פסיימים בהתחלה. איך נגיע אי פעם לCI, אם אפילו כיסוי חלקי של המערכת בטסטים נראה בלתי אפשרי? אבל גילינו שכיסוי של שינויים מביא את עיקר התועלת, כך שמה שלא משתנה ונשאר לא בדוק, הוא לא פקטור, כל עוד השינויים החדשים כן בדוקים.
לפני שכתבתי את השינוי, רציתי לבדוק אם אני יכול בכלל לשים את הקוד הזה בסביבת בדיקה. כמו שאתם רואים, כבר בקונסטרקטור הקלאס הזה צריך כל מני תלויות שלא קשורות בכלל לשינוי שאני הולך לעשות.
החלטתי לאמץ גישה אופטימית. הקוד שאני רוצה לשנות לא תלוי בדברים האלה, אז אם הקלאס יווצר כשהדברים האלה הם נאל, מה טוב. הוספתי טסט שלא עושה כלום, רק כדי לראות שהיצירה של הקלאס עוברת בלי תקלות.
הגישה האופטימית נחלה כשלון במקרה הזה. אפילו טסט ריק לא מצליח לרוץ בלי תקלות , בגלל שעצם היצירה של הקלאס צריכה אובייקטים אחרים, שאולי גם הם צריכים אובייקטים כדי להווצר כמו שצריך.היתה לי בנקודה הזו כמה אפשרויות: או לשנות את הקונסטרקטור של הקלאס כדי שאני אוכל ליצור אותו בלי תלויות, או לשנות את הטיפוסים כדי שאני אוכל ליצור את הקלאס עם זיופים של התלויות, או להוציא את הקוד שאני עובד עליו מהקלאס.מכל האפשרויות האלה, האחרונה היתה הכי פשוטה וכללה שינויים במקומות שאני רוצה לשנות בכל מקרה. האחרות כללו שינויים שלא קשורים בכלל לשינוי שאני רוצה, וגם נשמעו לי כמו שינויים הרבה יותר מסוכנים