SinonJS - Developing Backbone.js Applications (2013)

Developing Backbone.js Applications (2013)

Chapter 15. SinonJS

Similar to the section on testing Backbone.js apps using the Jasmine BDD framework, we’re nearly ready to take what we’ve learned and write a number of QUnit tests for our Todo application.

Before we start, though, you may have noticed that QUnit doesn’t support test spies. Test spies are functions that record arguments, exceptions, and return values for any of their calls. They’re typically used to test callbacks and how functions may be used in the application being tested. In testing frameworks, spies usually are anonymous functions or wrappers around functions that already exist.

What Is SinonJS?

In order for us to substitute support for spies in QUnit, we will be taking advantage of a mocking framework called SinonJS by Christian Johansen. We will also be using the SinonJS-QUnit adapter, which provides seamless integration with QUnit (meaning setup is minimal). SinonJS is completely test-framework−agnostic and should be easy to use with any testing framework, so it’s ideal for our needs.

The framework supports three features we’ll be taking advantage of for unit testing our application:

§ Anonymous spies

§ Spying on existing methods

§ A rich inspection interface

Basic Spies

Using this.spy() without any arguments creates an anonymous spy. This is comparable to jasmine.createSpy(). We can observe basic usage of a SinonJS spy in the following example:

test('should call all subscribers for a message exactly once', function () {

var message = getUniqueString();

var spy = this.spy();

PubSub.subscribe( message, spy );

PubSub.publishSync( message, 'Hello World' );

ok( spy.calledOnce, 'the subscriber was called once' );

});

Spying on Existing Functions

We can also use this.spy() to spy on existing functions (like jQuery’s $.ajax) in the example that follows. When we are spying on a function that already exists, the function behaves normally, but we get access to data about its calls, which can be very useful for testing purposes.

test( 'should inspect the jQuery.getJSON usage of jQuery.ajax', function () {

this.spy( jQuery, 'ajax' );

jQuery.getJSON( '/todos/completed' );

ok( jQuery.ajax.calledOnce );

equals( jQuery.ajax.getCall(0).args[0].url, '/todos/completed' );

equals( jQuery.ajax.getCall(0).args[0].dataType, 'json' );

});

Inspection Interface

SinonJS comes with a rich spy interface that allows us to test whether a spy was called with a specific argument, determine if it was called a specific number of times, and test against the values of arguments. You can find a complete list of features supported in the interface on SinonJS.org, but let’s take a look at some examples demonstrating some of the most commonly used ones.

Matching arguments: Test that a spy was called with a specific set of arguments

test( 'Should call a subscriber with standard matching': function () {

var spy = sinon.spy();

PubSub.subscribe( 'message', spy );

PubSub.publishSync( 'message', { id: 45 } );

assertTrue( spy.calledWith( { id: 45 } ) );

});

Stricter argument matching: Test that a spy was called at least once with specific arguments and no others

test( 'Should call a subscriber with strict matching': function () {

var spy = sinon.spy();

PubSub.subscribe( 'message', spy );

PubSub.publishSync( 'message', 'many', 'arguments' );

PubSub.publishSync( 'message', 12, 34 );

// This passes

assertTrue( spy.calledWith('many') );

// This however, fails

assertTrue( spy.calledWithExactly( 'many' ) );

});

Testing call order: Test that a spy was called before or after another spy

test( 'Should call a subscriber and maintain call order': function () {

var a = sinon.spy();

var b = sinon.spy();

PubSub.subscribe( 'message', a );

PubSub.subscribe( 'event', b );

PubSub.publishSync( 'message', { id: 45 } );

PubSub.publishSync( 'event', [1, 2, 3] );

assertTrue( a.calledBefore(b) );

assertTrue( b.calledAfter(a) );

});

Match execution counts: Test that a spy was called a specific number of times

test( 'Should call a subscriber and check call counts', function () {

var message = getUniqueString();

var spy = this.spy();

PubSub.subscribe( message, spy );

PubSub.publishSync( message, 'some payload' );

// Passes if spy was called once and only once.

ok( spy.calledOnce ); // calledTwice and calledThrice are also supported

// The number of recorded calls.

equal( spy.callCount, 1 );

// Directly checking the arguments of the call

equals( spy.getCall(0).args[0], message );

});

Stubs and Mocks

SinonJS also supports two other powerful features: stubs and mocks. Both stubs and mocks implement all of the features of the spy API, but have some added functionality.

Stubs

A stub allows us to replace any existing behavior for a specific method with something else. Stubs can be very useful for simulating exceptions and are most often used to write test cases when certain dependencies of your codebase may not yet be written.

Let us briefly re-explore our Backbone Todo application, which contained a Todo model and a TodoList collection. For the purpose of this walkthrough, we want to isolate our TodoList collection and fake the Todo model to test how adding new models might behave.

We can pretend that the models have yet to be written just to demonstrate how stubbing might be carried out. A shell collection containing only a reference to the model to be used might look like this:

var TodoList = Backbone.Collection.extend({

model: Todo

});

// Let's assume our instance of this collection is

this.todoList;

Assuming our collection is instantiating new models itself, it’s necessary for us to stub the model’s constructor function for the test. We can do this by creating a simple stub as follows:

this.todoStub = sinon.stub( window, 'Todo' );

The preceding creates a stub of the Todo method on the window object. When stubbing a persistent object, we must restore it to its original state. We can do this in a teardown() as follows:

this.todoStub.restore();

After this, we need to alter what the constructor returns, which we can do efficiently using a plain Backbone.Model constructor. While this isn’t a Todo model, it does still provide us an actual Backbone model.

setup: function() {

this.model = new Backbone.Model({

id: 2,

title: 'Hello world'

});

this.todoStub.returns( this.model );

});

The expectation here might be that this snippet would ensure our TodoList collection always instantiates a stubbed Todo model, but because a reference to the model in the collection is already present, we need to reset the model property of our collection as follows:

this.todoList.model = Todo;

The result of this is that when our TodoList collection instantiates new Todo models, it will return our plain Backbone model instance as desired. This allows us to write a test for the addition of new model literals as follows:

module( 'Should function when instantiated with model literals', {

setup:function() {

this.todoStub = sinon.stub(window, 'Todo');

this.model = new Backbone.Model({

id: 2,

title: 'Hello world'

});

this.todoStub.returns(this.model);

this.todos = new TodoList();

// Let's reset the relationship to use a stub

this.todos.model = Todo;

// add a model

this.todos.add({

id: 2,

title: 'Hello world'

});

},

teardown: function() {

this.todoStub.restore();

}

});

test('should add a model', function() {

equal( this.todos.length, 1 );

});

test('should find a model by id', function() {

equal( this.todos.get(5).get('id'), 5 );

});

});

Mocks

Mocks are effectively the same as stubs, but they mock a complete API and have some built-in expectations for how they should be used. The difference between a mock and a spy is that the expectations for mocks’ use are predefined and the test will fail if any of these are not met.

Here’s a snippet with sample usage of a mock based on PubSubJS. Here, we have a clearTodo() method as a callback and use mocks to verify its behavior.

test('should call all subscribers when exceptions', function () {

var myAPI = { clearTodo: function () {} };

var spy = this.spy();

var mock = this.mock( myAPI );

mock.expects( 'clearTodo' ).once().throws();

PubSub.subscribe( 'message', myAPI.clearTodo );

PubSub.subscribe( 'message', spy );

PubSub.publishSync( 'message', undefined );

mock.verify();

ok( spy.calledOnce );

});

Exercise

We can now begin writing tests for our Todo application, which are listed and separated by component (for example, models, collections, and so on). It’s useful to pay attention to the name of the test, the logic being tested, and most importantly the assertions being made, as this will give you some insight into how what we’ve learned can be applied to a complete application.

To get the most out of this section, I recommend looking at the QUnit Koans included in the practicals/qunit-koans folder—this is a port of the Backbone.js Jasmine Koans over to QUnit.

NOTE

In case you haven’t had a chance to try out one of the Koans kits yet, they are a set of unit tests using a specific testing framework that both demonstrate how a set of tests for an application may be written, but also leave some tests unfilled so that you can complete them as an exercise.

Models

For our models, we want to at minimum test that:

§ New instances can be created with the expected default values.

§ Attributes can be set and retrieved correctly.

§ Changes to state correctly fire off custom events where needed.

§ Validation rules are correctly enforced.

module( 'About Backbone.Model');

test('Can be created with default values for its attributes.', function() {

expect( 3 );

var todo = new Todo();

equal( todo.get('text'), '' );

equal( todo.get('done'), false );

equal( todo.get('order'), 0 );

});

test('Will set attributes on the model instance when created.', function() {

expect( 1 );

var todo = new Todo( { text: 'Get oil change for car.' } );

equal( todo.get('text'), 'Get oil change for car.' );

});

test('Will call a custom initialize function on the model instance when

created.', function() {

expect( 1 );

var toot = new Todo

({ text: 'Stop monkeys from throwing their own crap!' });

equal( toot.get('text'),

'Stop monkeys from throwing their own rainbows!' );

});

test('Fires a custom event when the state changes.', function() {

expect( 1 );

var spy = this.spy();

var todo = new Todo();

todo.on( 'change', spy );

// Change the model state

todo.set( { text: 'new text' } );

ok( spy.calledOnce, 'A change event callback was correctly triggered' );

});

test('Can contain custom validation rules, and will trigger an invalid

event on failed validation.', function() {

expect( 3 );

var errorCallback = this.spy();

var todo = new Todo();

todo.on('invalid', errorCallback);

// Change the model state in such a way that validation will fail

todo.set( { done: 'not a boolean' } );

ok( errorCallback.called, 'A failed validation correctly triggered an

error' );

notEqual( errorCallback.getCall(0), undefined );

equal( errorCallback.getCall(0).args[1], 'Todo.done must be a boolean

value.' );

});

Collections

For our collection we’ll want to test that:

§ The collection has a Todo model.

§ Uses localStorage for syncing.

§ That done(), remaining(), and clear() work as expected.

§ The order for todos is numerically correct.

describe('Test Collection', function() {

beforeEach(function() {

// Define new todos

this.todoOne = new Todo;

this.todoTwo = new Todo({

title: "Buy some milk"

});

// Create a new collection of todos for testing

return this.todos = new TodoList([this.todoOne, this.todoTwo]);

});

it('Has the Todo model', function() {

return expect(this.todos.model).toBe(Todo);

});

it('Uses localStorage', function() {

return expect(this.todos.localStorage).toEqual(new Store

('todos-backbone'));

});

describe('done', function() {

return it('returns an array of the todos that are done', function() {

this.todoTwo.done = true;

return expect(this.todos.done()).toEqual([this.todoTwo]);

});

});

describe('remaining', function() {

return it('returns an array of the todos that are not done', function() {

this.todoTwo.done = true;

return expect(this.todos.remaining()).toEqual([this.todoOne]);

});

});

describe('clear', function() {

return it('destroys the current todo from localStorage', function() {

expect(this.todos.models).toEqual([this.todoOne, this.todoTwo]);

this.todos.clear(this.todoOne);

return expect(this.todos.models).toEqual([this.todoTwo]);

});

});

return describe('Order sets the order on todos ascending numerically',

function() {

it('defaults to one when there arent any items in the collection',

function() {

this.emptyTodos = new TodoApp.Collections.TodoList;

return expect(this.emptyTodos.order()).toEqual(0);

});

return it('Increments the order by one each time', function() {

expect(this.todos.order(this.todoOne)).toEqual(1);

return expect(this.todos.order(this.todoTwo)).toEqual(2);

});

});

});

Views

For our views, we want to ensure:

§ They are being correctly tied to a DOM element when created

§ They can render, after which the DOM representation of the view should be visible

§ They support wiring up view methods to DOM elements

One could also take this further and test that user interactions with the view correctly result in any models that need to be changed being updated correctly.

module( 'About Backbone.View', {

setup: function() {

$('body').append('<ul id="todoList"></ul>');

this.todoView = new TodoView({ model: new Todo() });

},

teardown: function() {

this.todoView.remove();

$('#todoList').remove();

}

});

test('Should be tied to a DOM element when created, based off the property

provided.', function() {

expect( 1 );

equal( this.todoView.el.tagName.toLowerCase(), 'li' );

});

test('Is backed by a model instance, which provides the data.', function() {

expect( 2 );

notEqual( this.todoView.model, undefined );

equal( this.todoView.model.get('done'), false );

});

test('Can render, after which the DOM representation of the view will be

visible.', function() {

this.todoView.render();

// Append the DOM representation of the view to ul#todoList

$('ul#todoList').append(this.todoView.el);

// Check the number of li items rendered to the list

equal($('#todoList').find('li').length, 1);

});

asyncTest('Can wire up view methods to DOM elements.', function() {

expect( 2 );

var viewElt;

$('#todoList').append( this.todoView.render().el );

setTimeout(function() {

viewElt = $('#todoList li input.check').filter(':first');

equal(viewElt.length > 0, true);

// Ensure QUnit knows we can continue

start();

}, 1000, 'Expected DOM Elt to exist');

// Trigger the view to toggle the 'done' status on an item or items

$('#todoList li input.check').click();

// Check the done status for the model is true

equal( this.todoView.model.get('done'), true );

});

App

It can also be useful to write tests for any application bootstrap you may have in place. For the following module, our setup instantiates and appends to a TodoApp view, and we can test anything from local instances of views being correctly defined to application interactions correctly resulting in changes to instances of local collections.

module( 'About Backbone Applications' , {

setup: function() {

Backbone.localStorageDB = new Store('testTodos');

$('#qunit-fixture').append('<div id="app"></div>');

this.App = new TodoApp({ appendTo: $('#app') });

},

teardown: function() {

this.App.todos.reset();

$('#app').remove();

}

});

test('Should bootstrap the application by initializing the Collection.',

function() {

expect( 2 );

// The todos collection should not be undefined

notEqual( this.App.todos, undefined );

// The initial length of our todos should however be zero

equal( this.App.todos.length, 0 );

});

test( 'Should bind Collection events to View creation.' , function() {

// Set the value of a brand new todo within the input box

$('#new-todo').val( 'Buy some milk' );

// Trigger the enter (return) key to be pressed inside #new-todo

// causing the new item to be added to the todos collection

$('#new-todo').trigger(new $.Event( 'keypress', { keyCode: 13 } ));

// The length of our collection should now be 1

equal( this.App.todos.length, 1 );

});

Further Reading and Resources

That’s it for this section on testing applications with QUnit and SinonJS. I encourage you to try out the QUnit Backbone.js Koans and see if you can extend some of the examples. For further reading, consider looking at some of these additional resources:

§ Test-Driven JavaScript Development (book)

§ SinonJS/QUnit adapter

§ Using Sinon.JS with QUnit

§ Automating JavaScript Testing with QUnit

§ Unit Testing with QUnit

§ Another QUnit/Backbone.js demo project

§ SinonJS helpers for Backbone

Chapter 16. Conclusions

I hope that you’ve found this introduction to Backbone.js of value. What you’ve hopefully learned is that while building a JavaScript-heavy application using nothing more than a DOM manipulation library (such as jQuery) is certainly a possible feat, it is difficult to build anything nontrivial without any formal structure in place. Your nested pile of jQuery callbacks and DOM elements is unlikely to scale and can be very difficult to maintain as your application grows.

The beauty of Backbone.js is its simplicity. It’s very small given the functionality and flexibility it provides, which is evident if you begin to study the Backbone.js source. In the words of Jeremy Ashkenas: “The essential premise at the heart of Backbone has always been to try and discover the minimal set of data-structuring (Models and Collections) and user interface (Views and URLs) primitives that are useful when building web applications with JavaScript.” It just helps you improve the structure of your applications, helping you better separate concerns. There isn’t anything more to it than that.

Backbone offers models with key/value bindings and events, collections with an API of rich enumerable methods, declarative views with event handling, and a simple way to connect an existing API to your client-side application over a RESTful JSON interface. Use it, and you can abstract away data into sane models and your DOM manipulation into views, binding them together using nothing more than events.

Almost any developer working on JavaScript applications for a while will ultimately create a similar solution if that individual values architecture and maintainability. The alternative to using it or something similar is rolling your own—often a process that involves gluing together a diverse set of libraries that weren’t built to work together. You might use jQuery BBQ for history management and Handlebars for templating, while writing abstracts for organizing and testing code by yourself.

Contrast this with Backbone, which has literate documentation of the source code, a thriving community of both users and hackers, and a large number of questions about it asked and answered daily on sites like Stack Overflow. Rather than reinventing the wheel, you can reap the many advantages to structuring your application using a solution based on the collective knowledge and experience of an entire community.

In addition to helping provide sane structure to your applications, Backbone is highly extensible, supporting more custom architecture should you require more than what is prescribed out of the box. This is evident by the number of extensions and plug-ins that have been released for it over the past year, including some we have touched upon (such as MarionetteJS and Thorax).

These days, Backbone.js powers many complex web applications, ranging from the LinkedIn mobile app to popular RSS readers such as NewsBlur through to social commentary widgets such as Disqus. This small library of simple but sane abstractions has helped to create a new generation of rich web applications, and I and my collaborators hope that in time it can help you too.

If you’re wondering whether it is worth using Backbone on a project, ask yourself whether what you are building is complex enough to merit using it. Are you hitting the limits of your ability to organize your code? Will your application have regular changes to what is displayed in the UI without a trip back to the server for new pages? Would you benefit from a separation of concerns? If so, a solution like Backbone may be able to help.

Google’s Gmail is often cited as an example of a well-built single-page app. If you’ve used it, you might have noticed that it requests a large initial chunk, representing much of the JavaScript, CSS, and HTML most users will need, and everything extra needed after that occurs in the background. Gmail can easily switch between your inbox to your spam folder without having to rerender the whole page. Libraries like Backbone make it easier for web developers to create experiences like this.

That said, Backbone won’t be able to help if you’re planning to build something that isn’t worth the learning curve associated with a library. If your application or site will still be using the server to do the heavy lifting of constructing and serving complete pages to the browser, you may find just using plain JavaScript or jQuery for simple effects or interactions to be more appropriate. Spend time assessing how suitable Backbone might be for you, and make the right choice on a per-project basis.

Backbone is neither difficult to learn nor use; however, the time and effort you spend learning how to structure applications using it will be well worth it. While reading this book will equip you with the fundamentals you need to understand the library, the best way to learn is to try building your own real-world applications. You will hopefully find that the end product is cleaner, better organized, and more maintainable code.

With that, I wish you the very best with your onward journey into the world of Backbone and will leave you with a quote from American writer Henry Miller: “One’s destination is never a place, but a new way of seeing things.”