Reputation: 5464
I'm learning Backbone and want to "mock" the results of a .fetch()
call within a model. I do not want to use a testing library or actually hit an external service.
Basically I have a setting in my model, where if this.options.mock === true
, then just use an internal JSON object as the "result" of the fetch. Else, actually hit the API with a real AJAX request.
However, this doesn't seem to work. My view successfully renders with the model data when I hit the actual API ("real" fetch), but not whenever I try and pass in fake data.
Is there a way to fake a Fetch response in Backbone, without bringing in a testing library like Sinon?
here is the complete model (at least the relevant portions of it). Basically, the model fetches data, and formats it for a template. and then the view which owns the model renders it out.
'use strict';
(function (app, $, Backbone) {
app.Models.contentModel = Backbone.Model.extend({
/**
* Initializes model. Fetches data from API.
* @param {Object} options Configuration settings.
*/
initialize: function (options) {
var that = this;
that.set({
'template': options.template,
'mock': options.mock || false
});
$.when(this.retrieveData()).then(function (data) {
that.formatDataForTemplate(data);
}, function () {
console.error('failed!');
});
},
retrieveData: function () {
var that = this, deferred = $.Deferred();
if (typeof fbs_settings !== 'undefined' && fbs_settings.preview === 'true') {
deferred.resolve(fbs_settings.data);
}
else if (that.get('mock')) {
console.info('in mock block');
var mock = {
'title': 'Test Title',
'description': 'test description',
'position': 1,
'byline': 'Author'
};
deferred.resolve(mock);
}
else {
// hit API like normal.
console.info('in ajax block');
that.fetch({
success: function (collection, response) {
deferred.resolve(response.promotedContent.contentPositions[0]);
},
error: function(collection, response) {
console.error('error: fetch failed for contentModel.');
deferred.resolve();
}
});
}
return deferred.promise();
},
/**
* Formats data on a per-template basis.
* @return {[type]} [description]
*/
formatDataForTemplate: function (data) {
if (this.get('template') === 'welcomead_default') {
this.set({
'title': data.title,
'description': data.description,
'byline': data.author
});
}
// trigger the data formatted event for the view to render.
this.trigger('dataFormatted');
}
});
})(window.app, window.jQuery, window.Backbone);
Relevant bit from the view (ContentView):
this.model = new app.Models.contentModel({template: this.templateName});
this.listenTo(this.model, 'dataFormatted', this.render);
Is the data being set so fast that the listener hasn't been set up yet?
Upvotes: 1
Views: 3115
Reputation: 81
You can override the fetch function like this.
var MockedModel = Backbone.Model.extend({
initialize: function(attr, options) {
if (options.mock) {
this.fetch = this.fakeFetch;
}
},
url: 'http://someUrlThatWIllNeverBeCalled.com',
fakeFetch: function(options) {
var self = this
this.set({
'title': 'Test Title',
'description': 'test description',
'position': 1,
'byline': 'Author'
});
if (typeof options.success === 'function') {
options.success(self, {}, {})
}
}
});
var mockedModel = new MockedModel(null, {
mock: true
})
mockedModel.fetch({
success: function(model, xhr) {
alert(model.get('title'));
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.2/underscore-min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/backbone.js/1.1.2/backbone-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Upvotes: 2
Reputation: 17878
Problem here isn't with the actual implementation of retrieveData
but with the way it's being called. When you resolve the deferred before returning you're basically making it instant. This leads to formatDataForTemplate
being called while your model is still initializing.
So when you do
this.model = new app.Models.contentModel({template: this.templateName});
this.listenTo(this.model, 'dataFormatted', this.render);
The dataFormatted
event ends up being triggered before the listener has registered.
One solution is to use a timeout which should work with just
setTimeout(function() {
deferred.resolve(mock);
});
as that will delay the resolve untill the next round of the event loop when the listener is in place.
Another solution, not involving the setTimeout
would be to not call retrieveData
during model initialization but rather let the view do it after it has attached its listeners.
this.model = new app.Models.contentModel({template: this.templateName});
this.listenTo(this.model, 'dataFormatted', this.render);
this.model.retrieveData();
I would prefer the latter but if this is just about mocking data to work offline it doesn't really matter in my opinion.
Unrelated to that it's worth noting that the actual signature for initialize on a model is new Model([attributes], [options])
so your initialize should probably look like this
initialize: function (attributes, options) {
var that = this;
that.set({
'template': options.template,
'mock': options.mock || false
});
Just for the sake of readability. That again means that since you are passing only one object you should not need to call set
at all.
Upvotes: 1