Test-Driven Javascript with Jasmine
Jasmine
http://pivotal.github.com/jasmine/
Plugins we’ll use
https://github.com/pivotal/jasmine-ajax https://github.com/velesin/jasmine-jquery
The App We’ll Write
http://localhost:3000/
Anatomy of a Jasmine Test
describe( "ContactList", function() {
it( "loads my tests", function() {
expect( true ).toBeTruthy();
});
});
First failing spec
// spec/javascripts/contact-list-spec.js
describe( "ContactList", function() {
it( "accepts a container element in its constructor", function() {
new ContactList( $("#my-contact-list") );
});
});
Making it pass
// js/contact-list.js
;(function($) {
window.ContactList = function ( element ) {
}
ContactList.prototype = {
}
}(jQuery));
Spies
describe( "ContactList", function() {
it( "accepts a container element in its constructor", function() {
new ContactList( $("#my-contact-list") );
});
it( "creates a ContactListController with the element", function() {
var el = $("#my-contact-list");
spyOn( ContactList, "ContactListController" );
new ContactList( el );
expect( ContactList.ContactListController ).toHaveBeenCalledWith( el );
});
});
Spy failure
;(function($) {
ContactList.ContactListController = function( el ) {
}
}(undefined)); // Disallow jQuery access...
Making it pass
;(function($) {
window.ContactList = function ( element ) {
new ContactList.ContactListController( element );
}
}(jQuery));
Controller
Spying on a Constructor
describe( "ContactList.ContactListController", function() {
it( "creates a collection for the contacts", function() {
spyOn( ContactList, "ContactCollection" );
new ContactList.ContactListController();
expect( ContactList.ContactCollection ).toHaveBeenCalled();
});
});
Backbone.Collection
// js/contact-list/models/contact-collection.js
;(function($) {
ContactList.ContactCollection = Backbone.Collection.extend({
});
}(jQuery));
// js/contact-list/controllers/contact-list-controller.js
;(function($) {
ContactList.ContactListController = function( el ) {
new ContactList.ContactCollection();
}
}(undefined));
View
jasmine-jquery
describe( "ListView", function() {
var view;
describe( "#render", function() {
beforeEach( function() {
jasmine.getFixtures().set( "<div id='contact-list'></div>" );
var contacts = new ContactList.ContactCollection();
view = new ContactList.ListView({
model: contacts,
el: $("#contact-list")
});
view.render();
});
it( "creates a div container", function() {
expect( $("div.js-contact-list") ).toBeVisible();
});
});
});
Fail
Make it pass
;(function($) {
ContactList.ListView = Backbone.View.extend({
tagName: "div",
className: "js-contact-list",
initialize: function( options ) {},
render: function() {
this.$el.html( "<div class='js-contact-list'></div>" );
return this;
}
});
}(jQuery));
Passing
Templates and Fixtures
A More Robust View
<script type="text/template" id="list-template">
<div class="js-contact-list"></div>
</script>
;(function($) {
ContactList.ListView = Backbone.View.extend({
template: $("#list-template").html(),
initialize: function( options ) {},
render: function() {
this.$el.html( this.template );
return this;
}
});
}(jQuery));
More Spies
Spy Objects
describe( "ListView", function() {
describe( "when the collection adds an element", function() {
var newContact;
var contactView;
beforeEach( function() {
newContact = { name: 'Bob' };
_.extend( newContact, Backbone.Events );
contactView = jasmine.createSpyObj( "contactView", ["render"] );
});
it( "adds the ContactView's content to its own element", function() {
contactView.el = $("<p>Contact!</p>");
contactView.render.andReturn( contactView );
contacts.trigger( 'add', newContact );
expect( $("div.js-contact-list p") ).toBeVisible();
});
});
});
Testing AJAX
Mock-Ajax
describe( "ContactList.ContactCollection", function() {
var TestResponse = {
index: {
success: {
status: 200,
responseText: '{"contacts":[' +
'{"name":{"first":"Sim","last":"Wyman"},'
+ '"url":"http://www.schaefer.biz.biz","email":"selena@sanford.info",'
+ '"address":{"streetAddress":"4679 Leanne Branch Apt. 330",'
+ '"city":"East Dedrick","state":"Connecticut","zip":"50962"},'
+ '"phone":"1-237-138-5650 x1243","jabber":"aida@ondricka.biz"},' +
'{"name":{"first":"Flavio","last":"Hirthe"},'
+ '"url":"http://www.andersonbahringer.info.org","email":"felix@streichwolff.info",'
+ '"address":{"streetAddress":"5640 Anne Village Suite 123","city":"Hicklefort",'
+ '"state":"Oklahoma","zip":"64729"},'
+ '"phone":"1-540-742-1233 x43732","jabber":"laurine_bergnaum@murraykoch.name"}'
+ ']}'
}
}
};
var collection;
beforeEach( function() {
jasmine.Ajax.useMock();
collection = new ContactList.ContactCollection();
});
describe( "#fetch", function() {
beforeEach( function() {
collection.fetch();
request = mostRecentAjaxRequest();
request.response( TestResponse.index.success );
});
it( "fetches the current contacts from the server", function() {
expect( collection.size() ).toEqual( 2 );
});
});
});
Failure messages are…
Passes easily, however
// js/contact-list/models/contact-collection.js
ContactList.ContactCollection = Backbone.Collection.extend({
url: "/contacts",
model: ContactList.Contact,
parse: function( response ) {
return response.contacts;
}
});
Fin
All Done: http:localhost:3000
(I did skip a lot).