The aim of this post is to write a mock version of the $.ajax
that will let your code use promises and Backbone perform its stuff.
It is often required to test how collections and models behave when you ask them to fetch data from the server.
To test the flow completely, I prefer not to mock the sync
of fetch
methods. I instead do a step back and mock the $.ajax
, to make sure everything is working fine.
But I want to use promises
To add a layer of complexity, I really like to use promises when I fetch collections or model, like this:
myCollection.fetch(options).done(function () {
// cool stuff
});
Now, even if it is just a detail, when you mock the $.ajax
you need to keep that in mind, since you will also have to provide a promise as part of the mock.
Code setup
This is the simple collection we’ll be testing:
var Comments = Bakbone.Collection.extend({
url: '/comments'
});
And this is the JSON we expect from the server:
[
{ "name" : "Richard",
"text" : "What a beautiful day"
},
{ "name" : "Mark",
"text" : "What a rainy day"
}
]
When we fetch this data from the server, we expect to get two models inside our collection.
Our simple test, just to show how to mock the $.ajax
method and be able to use promises:
describe('The Comments collection', function () {
beforeEach(function () {
this.collection = new Comments();
});
it('should fetch data from the server', function (done) {
// If the 'done' method is called,
// the $.ajax correctly return a promise
this.collection.fetch().done(function () {
// If the collection's length is as expected
// the $.ajax is letting Backbone do it's magic
expect(this.collection.length).toEqual(2);
done();
});
});
}),
First attempt to mock $.ajax
We can start writing a simple mock for the $.ajax
this way:
spyOn($, 'ajax').and.callFake(function () {
var d = $.Deferred(),
response = {
your_response
};
// Resolving the promise
d.resolve(response);
// Returning the promise to be used in our code
return d.promise();
});
We are creating a Deferred
object and returning a promise
, the read-only version of the deferred. This will let our test go into the done
callback. Anyway, even if it passes the first step, the expectation will fail: the lenght of the collection is 0.
Delving into Backbone code: fetch
To understand why our code is not completely working, we need to know more about the Backbone implementation: check the fetch
source code for the Collection.
// From the Backbone source code...
fetch: function (options) {
...
options.success = function (resp) {
// Something to do when the data come back
};
...
return this.sync('read', this, options);
},
...
As you can see, Backbone prepares a options.success
callback and passes it to the sync
method (return this.sync('read', this, options)
), that will pass it straight to the $.ajax
method.
Bingo!
In our case, we are resolving a promise, but not calling any callback, so Backbone will never get any data back from the call!
A working mock of $.ajax
Keeping in mind that the success
callback is passed as part of the options
object to the $.ajax
method, all we have to do is call it passing our response
.
spyOn($, 'ajax').and.callFake(function (options) {
var d = $.Deferred(),
response = {
your_response
};
// Resolving the promise
d.resolve(response);
// Calling the Backbone's callback
options.success(response);
// Returning the promise to be used in our code
return d.promise();
});
Try again, and your test will pass.
A good way to learn new things is always to look at the source code :-)
Riccardo