שלום אורח התחבר

איך שיפרתי בדיקות יחידה לתוכנית Backbone באמצועת שינוי קוד התוכנית

נושאים:פיתוח צד-לקוח

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

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

1מה קורה כשמנסים לבדוק קוד מקולקל

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

https://github.com/ryan-roemer/backbone-testing

הקוד הרלוונטי למיון נמצא בקובץ notes-filter.js, הנה קטע להמחשה:


filterNote: function (model) {

  var $note = $("#" + model.id),

  match = this.isMatch(this.query(), model.get("title"));

 // Show matches, else hide.
 match ? $note.show() : $note.hide();
},

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

 

2אז מה עושים?

בדיקות יחידה טובות מתחילות בקוד שקל לבדוק אותו. הרציניים בינינו אף כותבים את הבדיקות לפני הקוד, מה שמכונה TDD (או BDD, תלוי את מי שואלים). איך היה אפשר לכותב את קוד הסינון בצורה נקייה יותר? התשובה ההגיונית היא שסינון צריך להתבצע בשכבת המידע ולא בשכבת התצוגה, כלומר אנו נרצה להעביר את כל פונקציונאליות הסינון ל Collection שאוסף את הפתקים.
אבל רגע, אתם וודאי תוהים, אם הסינון ימחק פתקים משכבת המידע — האם לא נאבד פתקים אלו לעד? האם לא נצטרך לדאוג שהמידע המסונן לא יישלח לשרת ? טוב אז כמובן שלא. אנו פשוט נשתמש בשני Collections: אחד לתצוגה והשני לשמירת המידע ותקשורת עם השרת. נייצר גם חיבור בין שני ה Collections כך שאוסף מידע התצוגה ישאב את הנתונים שלו ויתעדכן תמידית מה Collection הראשי.
הרעיון של Collection למיון ה״מולבש״ על Collection אחר הוא מימוש של תבנית עיצוב שנקראת Decorator. אפשר למצוא מימוש חופשי בגיטהאב בתור תוסף ל Backbone בכתובת:

https://github.com/jmorrell/backbone-filtered-collection

3הקוד לאחר השכתוב

את הקוד המלא לאחר השכתוב אפשר למצוא ב Repository משוכפל שלי יחד עם קוד הבדיקות הרלוונטי בכתובת:

https://github.com/ynonp/backbone-testing/

קוד ה view התקצר לשליש מגודלו המקורי, וכעת הקוד במלואו נראה כך:


App.Views.NotesFilter = Backbone.View.extend({

    el: ".navbar-search",

    events: {
      // Disable form submission.
      "submit": function () { return false; },

      // Call filter on any data change.
      "change   .search-query": "filterNotes",
      "input    .search-query": "filterNotes"
    },

    // Apply filter to all notes in collection.
    filterNotes: function () {
      var query = this.$(".search-query").val().trim();
			this.collection.filterBy(query);
    },

  });

ומה בנוגע למנגנון הסינון? אותו העברנו ל Collection, ואכן קוד זה התארך בכ-20 שורות, אבל כעת אנו יכולים לכתוב בדיקות יחידה לקוד הסינון בלבד, בלי לערב את התצוגה, ובדיקות יחידה אחרות לקוד התצוגה בלבד בלי לערב את קוד הסינון.
אני מביא כאן להמחשה כיצד ייראו בדיקות יחידה למנגנון הסינון, שימו לב שאין כלל נגיעה לתצוגה:


describe("App.Collections.Notes", function () {
	var notes = [
		{ title: "just a test", id: 1 },
		{ title: "another note", id: 2 },
		{ title: "just another note", id: 3 }
	];

	it('should filter notes for display', function() {
		var col = new App.Collections.Notes(notes);
		col.filterBy('just');

		expect(col).to.have.length(notes.length);
		expect(col.forDisplay).to.have.length(2);
		expect(col.forDisplay.get(1)).to.be.ok;
		expect(col.forDisplay.get(3)).to.be.ok;
		expect(col.forDisplay.get(2)).to.not.be.ok;
	});

	it('should reset filters when asked', function() {
		var col = new App.Collections.Notes(notes);
		col.filterBy('just');
		col.showAll();

		expect(col.forDisplay).to.have.length(notes.length);
		expect(col).to.have.length(notes.length);

	});
});

 

4וכעת נעבור לבדיקת ה View

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


describe("App.Views.NotesFilter", function () {
	var displayCollection = {
		filterBy: sinon.spy(),
		showAll: sinon.spy()
	};

	before(function() {
		var fixture = "<form class='navbar-search'><input class='search-query' /></form>";
		$('#fixtures').html(fixture);
	});

	it('should call filterBy when text changes in the input field', function() {
		var view = new App.Views.NotesFilter({collection: displayCollection});

		$('.search-query').val('foo');
		$('.search-query').trigger('input');

		expect(displayCollection.filterBy).to.have.been.calledWith('foo');
	});
});

 

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

מעדיפים לקרוא מהטלגרם? בקרו אותנו ב:@tocodeil

או הזינו את כתובת המייל וקבלו את הפוסט היומי בכל בוקר אליכם לתיבה:


נהניתם מהפוסט? מוזמנים לשתף ולהגיב