מבנה בדיקת יחידה

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

1. חלוקה לפרקים, תתי פרקים ובדיקות

קובץ בדיקות יחידה מגדיר אוסף של בדיקות מחולקות לפרקים ולתתי פרקים. מבנה קובץ הבדיקות מגדיר את הדו"ח שספריית הבדיקות תדפיס לאחר כל הרצה. נתבונן בתרשים הבא המציג דו"ח תוצאה של בדיקות:

![assets/03-teststructure/report.png]

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

מבנה הקוד המתאים לתרשים זה בג'סמין יראה כך, ויכתב בקובץ בתיקיית spec:

describe('Array', function() {
    describe('#indexOf', function() {
        it('should return -1 if element not found', function() {
            // test code here...
        });

        it('should return the index if element is found', function() {
            // test code here ...
        });
    });
});

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

2. תחביר הבדיקות

הבדיקות עצמן משתמשות באוביקט expect כדי לציין מה אמורה להיות התוצאה. מבנה אידאלי של בדיקה מחולק ל-3 חלקים:

  1. יצירת השחקנים (האוביקטים) הלוקחים חלק בבדיקה.
  2. ביצוע פעולה בודדת.
  3. בדיקת תוצאת הפעולה.

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

** איכות בדיקת יחידה נקבעת לפי הספציפיות שלה: יש לוודא שכשלון בבדיקה יוביל אתכם מיד לתיקון התקלה וכבר יציג מה נשבר **

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

describe('Array', function() {
    describe('#indexOf', function() {
        it('should return -1 if element not found', function() {
            const haystack = [10, 20, 30, 40];
            const needle   = 50;

            const index = haystack.indexOf(needle);

            expect(index).toEqual(-1);
        });

        it('should return the index if element is found', function() {
            const haystack = [10, 20, 30, 40];
            const needle   = 20;

            const index = haystack.indexOf(needle);

            expect(index).toEqual(1);
        });
    });
});

3. הגדרת מחלקה ובדיקות עבורה

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

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

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

נתחיל מקוד הבדיקה עבור המחלקה ולאחר מכן נלך לממש את קוד המשחק, זכרו עדיין ללא ממשק משתמש:

import Messages from 'messages';
import Game from 'game';

describe('Number Guessing Game', function() {
  describe('Valid Gameplay', function() {
    it('should say "Too Low" when number is too low', function() {
      const game = new Game(94);
      const result = game.guess(20);
      expect(result).toEqual(Messages.TOO_LOW);
    });

    it('should say "Too High" when number is too high', function() {
      const game = new Game(94);
      const result = game.guess(120);
      expect(result).toEqual(Messages.TOO_HIGH);
    });

    it('should say "Bravo!" when number is the same', function() {
      const game = new Game(94);
      const result = game.guess(94);
      expect(result).toEqual(Messages.WIN);
    });
  });

  describe('Exceptions', function() {
    it('should say "Invalid Input" when guessing a string', function() {
      const game = new Game(94);
      const result = game.guess('20');
      expect(result).toEqual(Messages.INVALID_INPUT);
    });

    it('should say "Invalid Input" when guessing an array', function() {
      const game = new Game(94);
      const result = game.guess([1,2,3]);
      expect(result).toEqual(Messages.INVALID_INPUT);
    });

    it('should say "Invalid Input" when guessing an object', function() {
      const game = new Game(94);
      const result = game.guess({a: 10});
      expect(result).toEqual(Messages.INVALID_INPUT);
    });
  });
});

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

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

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

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

4. סיכום

קוד התוכנית זמין בתיקית הדוגמאות או לצפיה אונליין בקישור: https://github.com/tocodeil/javascript-testing-course-examples/tree/master/03-teststructure

תוכנית הבדיקה שהוצגה בפרק:

import Messages from 'messages';
import Game from 'game';

describe('Number Guessing Game', function() {
  describe('Valid Gameplay', function() {
    it('should say "Too Low" when number is too low', function() {
      const game = new Game(94);
      const result = game.guess(20);
      expect(result).toEqual(Messages.TOO_LOW);
    });

    it('should say "Too High" when number is too high', function() {
      const game = new Game(94);
      const result = game.guess(120);
      expect(result).toEqual(Messages.TOO_HIGH);
    });

    it('should say "Bravo!" when number is the same', function() {
      const game = new Game(94);
      const result = game.guess(94);
      expect(result).toEqual(Messages.WIN);
    });
  });
});


קוד התוכנית זמין בתיקית הדוגמאות או לצפיה אונליין בקישור: https://github.com/tocodeil/javascript-testing-course-examples/tree/master/03-teststructure

תוכנית הבדיקה שהוצגה בפרק:

import Messages from 'messages';
import Game from 'game';

describe('Number Guessing Game', function() {
  describe('Valid Gameplay', function() {
    it('should say "Too Low" when number is too low', function() {
      const game = new Game(94);
      const result = game.guess(20);
      expect(result).toEqual(Messages.TOO_LOW);
    });

    it('should say "Too High" when number is too high', function() {
      const game = new Game(94);
      const result = game.guess(120);
      expect(result).toEqual(Messages.TOO_HIGH);
    });

    it('should say "Bravo!" when number is the same', function() {
      const game = new Game(94);
      const result = game.guess(94);
      expect(result).toEqual(Messages.WIN);
    });
  });
});