Testing the UntestableTaming production codeRoy KleinFlash Client Team LeaderWix.com
OverviewTest Driven Development OverviewThe motivations for and challenges of testing our production codeTechniques + a real life example
Unit TestingExercise classes in an isolated environmentVerify that actions on the code produce predictable, consistent resultsWrite TestsWrite CodePass All TestsRefactor
The price of Unit Testing New logic is more time consuming to writeOld logic may require extensive Refactoring to be testableAmount of code to maintain increasesIllusion of being bug free when tests passNo Stage in FlexUnitThe actual visual output can’t be fully tested
The benefits of Unit TestingBetter definition and understanding of the code’s purposeHelps improve code designEmpowers refactoringDocuments the usage of the codeA small, quick environment to debug inProvides immediate feedback when the code changes (Adding/modifying functionality, Refactoring, Merging)
TDD ROI conclusionsThe price:Initial additional development timeThe benefit:Cumulative reduction in defectsThe conclusion:No brainer for long term projects
The ChallengesClasses and functions have multiple responsibilitiesClasses are strongly coupled to other classesSome of these couplings are to SingletonsAmbiguous dependencies (Objects, callbacks)In short, code changes have unknown side effects that are hard to foresee
The MotivationOur production code is the result of several years of  rat chase Rapid DevelopmentRefactoring core code is dangerousAugmenting core capabilities is expensiveReducing code volatility may save us from a dangerous rewrite, or allow us to reuse code when a rewrite occursMoving to CI is key to remaining competitive
Legacy Testing Heuristic Before making the change, write passing tests for the area around your changeThese tests are intended to preserve the behavior of the code prior to your changeAdd failing tests that define the change you’re makingApply your change and make all tests pass
Replace Dialog: Dependency Breaking
Replace Dialog: Dependency Breaking
Case Study 1: Replace Gallery
Safe Modification: Function extractionPreserve the original functions’ signature.
1. Preserve function signature
Safe Modification: Function extractionPreserve the original functions’ signature. Extract the function to its own class
2. Extract the modified function
Safe Modification: Function extractionPreserve the original functions’ signature. Extract the function to its own classCall the new function with the same signature, and a reference to an interface representing the original class
3. Reference original class
Safe Modification: Function extractionPreserve the original functions’ signature. Extract the function to its own classCall the new function with the same signature, and a reference to an interface representing the original classUse the IDE to determine what calls are to be redirected to the original class
4. Use IDE to find what’s missing
Safe Modification: Function extractionPreserve the original functions’ signature. Extract the function to its own classCall the new function with the same signature, and a reference to an interface representing the original classUse the IDE to determine what calls are to be redirected to the original classPut those calls in the interface
5. Put what’s missing in the interface
5. Put what’s missing in the interface
Safe Modification: Function extractionPreserve the original functions’ signature. Extract the function to its own classCall the new function with the same signature, and a reference to an interface representing the original classUse the IDE to determine what calls are to be redirected to the original classPut those calls in the interfaceCreate a mock class implementing the interface for testing
6. Create a mock class
Test can now execute!
Breaking encapsulation on purposeEncapsulation is a tool that makes the code usage easier to understandThe need to break it often points to problem in the design of the code usageProblems in design can’t be solved without Unit Tests in placeThe break can help in understanding what needs to be changedDamage can be minimized with self documenting code
Breaking encapsulation on purpose
Solving Singletons dependencyYou can’t use a testing instanceCreate a testing only function to set a testing instance
Solving Static dependenciesYou can’t subclass or mock a static classReplace reference to static variables with getters and static functions to local functionsSubclass and override with a test class to use your own data/logicIn 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
ConclusionGetting legacy code into a testing environment is possibleMost of the benefits of TDD can be achieved with partial coverageAdapting TDD during any stage of the development is worthwhile. The bigger the project, the more beneficial it is
Q & AAgile Principles, Patterns and Practices: Robert Martin

Testing the untestable

  • 1.
    Testing the UntestableTamingproduction codeRoy KleinFlash Client Team LeaderWix.com
  • 2.
    OverviewTest Driven DevelopmentOverviewThe motivations for and challenges of testing our production codeTechniques + a real life example
  • 3.
    Unit TestingExercise classesin an isolated environmentVerify that actions on the code produce predictable, consistent resultsWrite TestsWrite CodePass All TestsRefactor
  • 4.
    The price ofUnit Testing New logic is more time consuming to writeOld logic may require extensive Refactoring to be testableAmount of code to maintain increasesIllusion of being bug free when tests passNo Stage in FlexUnitThe actual visual output can’t be fully tested
  • 5.
    The benefits ofUnit TestingBetter definition and understanding of the code’s purposeHelps improve code designEmpowers refactoringDocuments the usage of the codeA small, quick environment to debug inProvides immediate feedback when the code changes (Adding/modifying functionality, Refactoring, Merging)
  • 6.
    TDD ROI conclusionsTheprice:Initial additional development timeThe benefit:Cumulative reduction in defectsThe conclusion:No brainer for long term projects
  • 7.
    The ChallengesClasses andfunctions have multiple responsibilitiesClasses are strongly coupled to other classesSome of these couplings are to SingletonsAmbiguous dependencies (Objects, callbacks)In short, code changes have unknown side effects that are hard to foresee
  • 8.
    The MotivationOur productioncode is the result of several years of rat chase Rapid DevelopmentRefactoring core code is dangerousAugmenting core capabilities is expensiveReducing code volatility may save us from a dangerous rewrite, or allow us to reuse code when a rewrite occursMoving to CI is key to remaining competitive
  • 9.
    Legacy Testing HeuristicBefore making the change, write passing tests for the area around your changeThese tests are intended to preserve the behavior of the code prior to your changeAdd failing tests that define the change you’re makingApply your change and make all tests pass
  • 10.
  • 11.
  • 12.
    Case Study 1:Replace Gallery
  • 13.
    Safe Modification: FunctionextractionPreserve the original functions’ signature.
  • 14.
  • 15.
    Safe Modification: FunctionextractionPreserve the original functions’ signature. Extract the function to its own class
  • 16.
    2. Extract themodified function
  • 17.
    Safe Modification: FunctionextractionPreserve the original functions’ signature. Extract the function to its own classCall the new function with the same signature, and a reference to an interface representing the original class
  • 18.
  • 19.
    Safe Modification: FunctionextractionPreserve the original functions’ signature. Extract the function to its own classCall the new function with the same signature, and a reference to an interface representing the original classUse the IDE to determine what calls are to be redirected to the original class
  • 20.
    4. Use IDEto find what’s missing
  • 21.
    Safe Modification: FunctionextractionPreserve the original functions’ signature. Extract the function to its own classCall the new function with the same signature, and a reference to an interface representing the original classUse the IDE to determine what calls are to be redirected to the original classPut those calls in the interface
  • 22.
    5. Put what’smissing in the interface
  • 23.
    5. Put what’smissing in the interface
  • 24.
    Safe Modification: FunctionextractionPreserve the original functions’ signature. Extract the function to its own classCall the new function with the same signature, and a reference to an interface representing the original classUse the IDE to determine what calls are to be redirected to the original classPut those calls in the interfaceCreate a mock class implementing the interface for testing
  • 25.
    6. Create amock class
  • 26.
    Test can nowexecute!
  • 27.
    Breaking encapsulation onpurposeEncapsulation is a tool that makes the code usage easier to understandThe need to break it often points to problem in the design of the code usageProblems in design can’t be solved without Unit Tests in placeThe break can help in understanding what needs to be changedDamage can be minimized with self documenting code
  • 28.
  • 29.
    Solving Singletons dependencyYoucan’t use a testing instanceCreate a testing only function to set a testing instance
  • 30.
    Solving Static dependenciesYoucan’t subclass or mock a static classReplace reference to static variables with getters and static functions to local functionsSubclass and override with a test class to use your own data/logicIn 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.
    ConclusionGetting legacy codeinto a testing environment is possibleMost of the benefits of TDD can be achieved with partial coverageAdapting TDD during any stage of the development is worthwhile. The bigger the project, the more beneficial it is
  • 32.
    Q & AAgilePrinciples, Patterns and Practices: Robert Martin

Editor's Notes

  • #3 אני אעבור מאוד בקצרה על יוניט טסטינג, יתרונות, חסרונות, אח"כ אני אספר קצת על הרקע להחלטה של שינוי מתודולוגיית הפיתוח, וההשלכות המקצועיות של ההחלטה הזו, ולבסוף אני אעבור על טכניקות פיתוח של הכנסת קוד שעל פניו נראה בלתי אפשרי לבדיקה, לתוך סביבת בדיקה בצורה בטוחה, בלי לשנות את ההתנהגות שלו בתהליך, עם דוגמה אמיתית ממצולות הקוד בייס של וויקס.
  • #4 אז קודם כל קצת על יוניט טסטינג, האבן יסוד של TDD. על רגל אחת, מדובר על קוד שמריץ קוד ובודק את התוצאות בהנתן כל מני אינפוטים.בתחילת כל טסט, אינסנטס חדש של הקלאס נוצר, ופעולה אחת בלבד מתבצעת – בד"כ קיראה לפונקציה עם ערך כלשהו. לבסוף, בודקים שהפעולה גרמה לתוצאה הצפויה. אם כן, הטסט עובר. אם לא, הטסט נכשל.הסייקל פיתוח ב TDDהוא פחות או יותר ככה: מפתח כותב פעולה אטומית, שיכולה להיות פיצ'ר, חלק מפיצ'ר, תיקון באג, בד"כ לא יותר מכמה שורות קוד, ובמבקביל כותב טסטים או מתקן טסטים כדי לבדוק שהקוד שהוא הרגע כתב עושה מה שהוא אמור לעשות. בגישה הקלאסית אומרים תמיד לכתוב הטסט קודם ואז את הקוד הפשוט ביותר שגורם לטסט הזה לעבור, אבל אני לא דוגמטי לגבי הסדר, וכך גם רוב מפתחי ה TDD שדיברתי איתם.אחרי שהפיצ'ר כתוב וכל הטסטים כתובים ועוברים, אפשר לשכתב את הקוד כך שיהיה יותר יפה או יותר יעיל. כיוון שיש לנו טסטים שמוודאים את תקינות הקוד, אנחנו חופשיים לשכתב בסייקלים מאוד מהירים ובלי המון מחשבה על שינוי פוקציונליות, אם עשינו טעות, לפחות טסט אחד אמור להכשל, ומייד יש לנו פידבק שעשינו טעות.ולבסוף, כמובן חוזרים וממשיכים בכתיבה של פעולה אטומית חדשה.
  • #5 אני לא אעבור על כל הנקודות, רק אציין שבגדול אפשר לומר שזמן הפיתוח מתארך כשעובדים עם טסטים. מן הסתם, מדובר בעוד קוד שצריך לכתוב.יוניט טסטים גם לא נועדו לבדוק ממשק או תוצאות ויזואליות. לדוגמא, המנגנון צביעה של וויקס נבדק ע"י טסטים בכל שלב בתהליך, חוץ מהאם באמת בסוף אובייקטים נצבעו כמו שהם אמורים להצבע.
  • #6 כל הנקודות האלה מסתכמות פחות או יותר בכך שלאורך זמן, כמות הבאגים שהמוצר צובר פחותה בהרבה ביחס לפיתוח ללא טסטינג. זוהי נקודה שנבדקה ע"י מספר מחקרים ונמצאה כנכונה.
  • #7 אז החסרון העיקרי של יוניט טסטינג זה זמן פיתוח ראשוני ארוך יותר, והיתרון העיקרי של יוניט טסטינג זה ירידה משמעותית בהצטברות באגים. אז לפרוייקטים ארוכי טווח, עבודה עם יוניט טסטים צריכה להיות החלטה יד מובנת מאליה.י
  • #8 עם קצת נסיון ביוניט טסטינג, בדיקה של קוד חדש היא עניין של מה בכך. האתגר הטכני הוא, כיצד בודקים קוד ישן שצריך לבצע בו שינויים? קלאסים שלא נכתבו במתודולוגיה של טסטים נוטים להיות מאוד תלותיים בקלאסים אחרים, פשוט כי יותר טבעי לכתוב עם תלות מאשר בלי. הנה דוגמה מהקוד בייס שלנו. לאחרונה הכנסנו את האדיטור שלנו לתוך מעטפת פלקס, מתוך כוונה להמיר לאט לאט את ה UI שלנו לפלקס. יש לנו דיאלוג להוספת אייטמים, שמשתמשים בה בהרבה מקומות במוצר, ובין השאר מתוך דיאלוג שהומר לפלקס. כדי לפתור כל מני בעיות, היינו צריכים שהדיאלוג הוספה הזה, כשהוא נפתח מהדיאלוג הפלקסי, ייפתח בתוך מעטפת פלקסית גם הוא. הקוד שפותח את הדיאלוג הוספה נמצא בתוך קלאס מאוד ישן ומאוד גדול, שנקרא EdWizardManager
  • #9 אנחנו בוויקס עובדים על פרוייקט ארוך טווח, ולא אימצנו טסטינג מתחילת הדרך. בתחילת קיומה, כמו כל סטארט אפ, וויקס היתה במירוץ כנגד השעון, להגיע למוצר מוכר לפני שכסף ההשקעה זולג או שמתחרים משתלטים על השוק עם מוצר מתחרה. הפיתוח המאוד מהיר הוכיח את עצמו בסופו של דבר, כשוויקס הצליחה לצאת מנצחת עם מוצר מוביל בשוק, אבל הקוד בייס של המוצר הגיע למצב שהיה ריסקי לבצע בו שינויים. מצד אחד המון לקוחות תלויים ביציבות שלו, ומצד שני הוא לא כתוב בצורה שנותנת פידבק על תוצאה של שינויים. גרסאות התחילו להתארך ולהסתבך, הQA נהיה תהליך יקר, והבנו שכדי להתקדם בצורה משמעותית מעתה והלאה, צריך לשנות את תפיסת הפיתוח.בהתחלה הוחלט על שכתוב. נשכרו אנשים והוקם צוות שהתחיל לעבוד על וויקס 2, אבל באותה תקופה גם התחילה הפניקה בתעשייה מהמהלך המוצהר של אפל כנגד פלאש. וויקס לא רצתה להשאר מחוץ לשוק ההולך וגדל של האייפונים, והוחלט לנטוש את עבודת השכתוב, לטובת כתיבת מוצר חדש שנותן שירות בניית אתרים דומה לוויקס בפלאש, אבל ב HTML. למרות שבאותה תקופה האמנתי בשכתוב, וגם רציתי להיות מעורב בשכתוב מתוך עניין מקצועי, בדיעבד אני מאוד שמח שנמנענו מזה. הסיכון והעלויות של שכתוב מוצר הם עצומים, ולאחר ששינינו את תפיסת הפיתוח על המוצר הקיים, הסתבר גם שהרבה מהבעיות של הקוד בייס המקורי הן פתירות. אז שינינו את תפיסת הפיתוח. הוחלט לאמץ בכל מחלקות הR&D חזון של CI, קוניטינוס אינטגריישן. בשביל הפלאש קליינט, שנכתב במתודולוגיה הפוכה לחלוטין לזו של CI, היינו די פסיימים בהתחלה. איך נגיע אי פעם לCI, אם אפילו כיסוי חלקי של המערכת בטסטים נראה בלתי אפשרי? אבל גילינו שכיסוי של שינויים מביא את עיקר התועלת, כך שמה שלא משתנה ונשאר לא בדוק, הוא לא פקטור, כל עוד השינויים החדשים כן בדוקים.
  • #11 לפני שכתבתי את השינוי, רציתי לבדוק אם אני יכול בכלל לשים את הקוד הזה בסביבת בדיקה. כמו שאתם רואים, כבר בקונסטרקטור הקלאס הזה צריך כל מני תלויות שלא קשורות בכלל לשינוי שאני הולך לעשות.
  • #12 החלטתי לאמץ גישה אופטימית. הקוד שאני רוצה לשנות לא תלוי בדברים האלה, אז אם הקלאס יווצר כשהדברים האלה הם נאל, מה טוב. הוספתי טסט שלא עושה כלום, רק כדי לראות שהיצירה של הקלאס עוברת בלי תקלות.
  • #13 הגישה האופטימית נחלה כשלון במקרה הזה. אפילו טסט ריק לא מצליח לרוץ בלי תקלות , בגלל שעצם היצירה של הקלאס צריכה אובייקטים אחרים, שאולי גם הם צריכים אובייקטים כדי להווצר כמו שצריך.היתה לי בנקודה הזו כמה אפשרויות: או לשנות את הקונסטרקטור של הקלאס כדי שאני אוכל ליצור אותו בלי תלויות, או לשנות את הטיפוסים כדי שאני אוכל ליצור את הקלאס עם זיופים של התלויות, או להוציא את הקוד שאני עובד עליו מהקלאס.מכל האפשרויות האלה, האחרונה היתה הכי פשוטה וכללה שינויים במקומות שאני רוצה לשנות בכל מקרה. האחרות כללו שינויים שלא קשורים בכלל לשינוי שאני רוצה, וגם נשמעו לי כמו שינויים הרבה יותר מסוכנים