בדיקות יחידה טובות מתחילות בקוד טוב

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

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

1. למה אנחנו לא מצליחים לכתוב בדיקות איכותיות

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

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

 

נעבור לקוד המשחק ונחפש רכיבים שניתן ״לנתק״ אותם מהמשחק ולבדוק אותם בפני עצמם. הקוד מורכב מהפונקציות: timeout, shuffle, check_round ו render. קחו לדוגמא את הפונקציה render:

function render() {
  el.score.innerHTML = score;
  
  var current_winner = el.game.querySelector('.winner');
  var next_winner = el.squares[winner_index];
  
  current_winner.classList.remove('winner');
  next_winner.classList.add('winner');
}

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

אותו הדין תופס גם על שאר הפונקציות. התלויות בין הפונקציות והתלות של הקוד במבנה ה DOM מביאים למצב שאין רכיב משמעותי לקוד שניתן ״לנתק״ אותו מהקוד ולבדוק אותו בנפרד.

2. הדרך הקשה לכתוב בדיקות יחידה.

הבעייה בכתיבת בדיקות יחידה היא בקוד. לכן הדרך לפתור אותה היא שינוי הקוד. נתבונן בקוד הבא שלוקח גישה שונה למימוש אותו המשחק בדיוק:

 

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

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

describe('Game Tests', function() {
  describe('Score', function() {

    it("should add 5 points for a winning round", function() {
      var game = new Game();
      var winner = _.find(game.getItems(), function(el) { return el.isWinner(); });

      game.round_end(winner);

      expect(game.getScore()).toEqual(5);
    });

    it('should take 5 points in a losing round', function() {
      var game = new Game();
      var loser = _.find(game.getItems(), function(el) { return ! el.isWinner(); });

      game.round_end(loser);

      expect(game.getScore()).toEqual(-5);
    });
  });
});

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

it('should always have just 1 winner', function() {
  var game = new Game();
  var item = game.getItems()[0];

  for ( var i=0; i < 10; i++ ) {
    game.round_end(item);

    var winners = _.filter(game.getItems(), function(item) { return item.isWinner(); });
    var losers = _.filter(game.getItems(), function(item) { return ! item.isWinner(); });

    // expect just one winner
    expect(winners.length).toEqual(1);

    // all others are losers
    expect(losers.length).toEqual(game.getItems().length - 1);
  }
});

כך נראה קוד הבדיקה במלואו:

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

3. סיכום וקריאת המשך

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

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

בנושא שכתוב קוד על מנת שיהיה קל יותר לבדיקה, אני ממליץ לקרוא את המאמר Writing Testable JavaScript של רבקה מרפי, המציג בהרחבה עקרונות פיתוח קוד שקל לבדוק אותו:
http://alistapart.com/article/writing-testable-javascript

ואפשר לקרוא עוד על ספריית Jasmine ויכולות הבדיקה אתה בקישור:
http://jasmine.github.io/2.2/introduction.html