I believe that I have more or less finished exploring testing Backbone.js with jasmine. Ready to tackle something new, I think I will try adding a different view to my calendar application. And what better way to add a new feature that driving it with BDD?
And lo! I know just the tool to BDD that feature. Looks like I am not quite done with Jasmine after all…
For my month view, I have just been stubbing (with sinon.js) server lookups to return a single appointment. For the list view, I think I ought to start by returning three appointments:
describe("list view", function() {
var couch_doc1 = {
"_id": "1",
"_rev": "1-1111",
"title": "Appt 001",
"startDate": "2011-10-01"
// ...
}
, couch_doc2 = { "_id": "2", /* ... */, "startDate": "2011-11-25" }
, couch_doc3 = { "_id": "3", /* ... */, "startDate": "2011-12-31" }
, doc_list = {
"total_rows": 3,
"rows": [
{"value": couch_doc2},
{"value": couch_doc1},
{"value": couch_doc3} ]
};
My backend is, of course, CouchDB—hence the structure of the document list that will be returned. I am intentionally mucking with the order of the documents because I am anticipating sorting them in the front-end.With the appointment documents ready, I write my spec setup, which does the usual of creating a new instance of my Backbone
Cal
application. It also stubs out server calls to return the document list from the main describe("list view", ...)
block: describe("list view", function() {
var couch_doc1 = { "_id": "1", /* ... */, "startDate": "2011-10-01" }
, couch_doc2 = { "_id": "2", /* ... */, "startDate": "2011-11-25" }
, couch_doc3 = { "_id": "3", /* ... */, "startDate": "2011-12-31" }
, doc_list = { /* rows: couch_doc2, couch_doc1, couch_doc3 */ }
beforeEach(function() {
window.calendar = new Cal($('#calendar'));
Backbone.history.navigate('#list', true);
// populate appointments for this month
server.respondWith('GET', /\/appointments/,
[200, { "Content-Type": "application/json" }, JSON.stringify(doc_list)]);
server.respond();
});
});
The only real difference between this setup and the setup for my month view is that I am navigating to '#list'
rather than '#month'
.On that list page, I expect to see 3 appointments. Or, in jasmine-ese:
describe("list view", function() {
var couch_doc1 = { "_id": "1", /* ... */, "startDate": "2011-10-01" }
, couch_doc2 = { "_id": "2", /* ... */, "startDate": "2011-11-25" }
, couch_doc3 = { "_id": "3", /* ... */, "startDate": "2011-12-31" }
, doc_list = { /* rows: couch_doc2, couch_doc1, couch_doc3 */ }
beforeEach(function() { /* ... */ });
it("lists all appointments", function() {
expect($("li", "#appointment-list").length).toEqual(3);
});
});
When I load the new "list view" specs in the browser, I get the expected failure:So my next question is, what do I need to do to change the failure message or make it pass? Simply asking the question makes me realize I have gotten ahead of myself. To do any of this, I am going to need a "list view". I do not even have a route that responds to such a thing, let alone a view.
So, I take a step back and mark this test pending (for now) and write a more appropriate first step:
describe("list view", function() {
var /* couch_doc1, couch_doc2, couch_doc3, doc_list */
beforeEach(function() { /* ... */ });
it("routes to the list view", function() {
expect($('h1')).toHaveText(/Appointment List/);
});
xit("lists all appointments", function() {
expect($("li", "#appointment-list").length).toEqual(3);
});
});
With that, I have a smaller failure:To be sure, I could make an even smaller first failure by testing the routing itself. I am jumping ahead of myself a little—hopefully I will not get burned. It turns out to require quite a few changes simply to get the new view to render. I have to add a new route, which calls a new method on my application view, which creates a new view type:
var Application = Backbone.View.extend({
// ...
setListView: function() {
this.view = 'list';
return this.render();
},
render: function() {
var view;
if (this.view == 'list') {
view = new CalendarList();
}
else {
var date = this.collection.getDate();
view = new CalendarMonth({date: date});
}
$(this.el).html(view.render().el);
},
// ...
});
var CalendarList = Backbone.View.extend({
render: function() {
$(this.el).html("<h2>Appointment List</h2>");
return this;
}
});
After all of that, I have my first passing test:That took a bit more effort than anticipated. I am pretty sure I have introduced a memory leak in there. I know that I am violating at least one Recipes with Backbone recipe ("Evented Routers"). But none of that matters: (1) get it done, (2) do it right, (3) optimize. I have reached #1 and that is good enough for now. I will worry about #2 and #3 another day if needed (and with the benefit of more tests).
For now, I would like to get my original test passing:
it("lists all appointments", function() {
expect($("li", "#appointment-list").length).toEqual(3);
});
In the spirit of "get it done", I pull out an Underscore.js reduce()
: var CalendarList = Backbone.View.extend({
initialize: function(options) {
options.collection.bind('reset', this.render, this);
},
render: function() {
$(this.el).html(
'<h2>Appointment List</h2>' +
'<ol id="appointment-list">' +
this.collection.reduce(function(memo, appointment) {
return memo + '<li>' + appointment.title + '</li>';
}, "") +
'<ol>'
);
return this;
}
});
To be sure, this is a very rudimentary listing of appointments. In the final version, I will certainly need to show more than a list of static titles. But it does enough to make my test pass:Since it works, that will serve as a stopping point tonight.
Day #116
One thing I notice is that you do not dispose of your views which might lead to zombie views.
ReplyDeleteI mention this in this post http://www.thesoftwaresimpleton.com/blog/2011/11/13/backbone-js---lessons-learned/.
Maybe the fact that you keep everything in memory means you do not have to dispose of them. It is something worth raising though
@Paul I think you're probably correct that I've got zombie views lurking about. Definitely something that I've got to address in the near future. Thanks!
ReplyDelete