Backbone, Marionette, Jasmine: How to test jQuery deferred event - backbone.js

I'm very new to Jasmine and Marionette and looking for some help on how to test and even just the proper way to think about testing my application. Any pointers are welcome.
I have a Marionette Controller that I use to fetch my model, instantiate my views and render them. I use a method found at the bottom of this page so that the model is fetched before the view is rendered: https://github.com/marionettejs/backbone.marionette/blob/master/upgradeGuide.md#marionetteasync-is-no-longer-supported.
My controller method to fetch the model and display the view looks like so:
showCaseById: function(id){
App.models.Case = new caseModel({ id: id });
var promise = App.models.Case.fetch();
$.when(promise).then(_.bind(this.showContentView, this));
},
As you can see, it calls the showContentView after the model is fetched. That method is here:
showContentView: function(model){
App.views.Body = new bodyView({
model: App.models.Case
});
App.views.Body.on('case:update', this.submitCase, this);
// this.layout is defined in the controller's initialize function
this.layout.content.show(App.views.Body);
},
What is the proper way to test this functionality? I'd like to test the calling of the showContentView function after the completion of the promise. How should I break up the specs for this?
Thanks.

First, spy on your showContentView method and assert it has been called:
it('showCaseById', function (done) {
var controller = new Controller();
spyOn(controller, 'showContentView');
controller.showCaseById('foo');
expect(controller.showContentView).toHaveBeenCalledWith(jasmine.any(caseModel));
});
Secondly, I would recommend you stub out the call to fetch() so you don't hit the network, but it's starting to get a bit hairy now:
function caseModel() {
this.fetch = function () {
// return a promise here that resolves to a known value, e.g. 'blah'
};
}
Now, you can have a slightly stronger assertion, but this is a bit shonky because you're fiddling around with internals of your dependencies:
expect(controller.showContentView).toHaveBeenCalledWith('blah');
By overriding caseModel, when your controller method goes to create one, it gets your new version instead of the old one, and you can control the implementation of the new one just for this test.
There are ways to make this code more testable, but as it seems you're just starting out with testing I won't go into it all. You'll certainly find out those things for yourself as you do more testing.

First, it's important to understand that _.bind(fn, context) doesn't actually call fn. Instead, it returns a function that when called will call fn(). The context defines the object that fn will use internally as this.
It's not necessary but you could write showCaseById as :
showCaseById: function(id){
App.models.Case = new caseModel({ id: id });
var promise = App.models.Case.fetch();
var fn = _.bind(this.showContentView, this);
$.when(promise).then(fn);
},
As I say, that is unnecessary but now you understand that _.bind() returns a function and that $.when(promise).then(...) accepts a function as its (first) argument.
To answer the actual question, you can confirm that the App.models.Case.fetch() promise has been fulfilled by adding a further $.when(promise).then(...) statement, with a test function of your own choosing.
showCaseById: function(id){
App.models.Case = new caseModel({ id: id });
var promise = App.models.Case.fetch();
$.when(promise).then(_.bind(this.showContentView, this));
// start: test
$.when(promise).then(function() {
console.log("caseModel " + id + " is ready");//or alert() if preferred
});
// fin: test
},
The second $.when(promise).then(...) will not interfere with the first; rather, the two will execute sequentially. The console.log() satatement will provide reliable confirmation that the this.showContentView has been called successfully and that initial rendering should have happened.
If nothing is rendered at this point or subsequently, then you must suspect that this.showContentView needs to be debugged.

Related

Pre-loading dependent promises in Angular 1

Bear with me as this is hard for me to wrap my head around. I have an object that we pull from a database that is mapped with numbers as keys:
entity: {
23: "This is a definition of the string here",
34: "This is a string definition for 34 here",
55: ['array', 'here'],
...
}
Let's imagine the "23" key refers to the name of the author and "34" refers to the name of the publisher and so on. I don't want to manually map out these relationships in each controller. As such, I would think that the best place to do the mapping is in a Service named something like "BookService".
I started by creating a bunch of functions in the BookService like:
vm.getAuthor = function () {
return vm.entity[23];
}
...
Per this style guide (https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md), I tried to then do:
vm.getAuthor = getAuthor;
function getAuthor() { return vm.entity[23]; }
Is this the right way to approach this? The major problems I've run into are:
I get "Error: 10 $digest() iterations reached. Aborting!" errors. Based on what I've read on SO, not sure how I am supposed to move the definition out of the function?
I am resolving the "entity" attribute in the UI-router prior to the controller. However, the BookService doesn't get this until I pass it in. So, for each function, do I need to send in the entity attribute? For example, change the function to something like:
function getAuthor(entity) { return entity[23]; }
I'm still a beginner with angular, so hopefully this makes sense. Let me know if it needs further clarification!
Seems like it would be better to create a factory that creates books from a supplied entity:
angular.module('myapp')
.factory('BookFactory', BookFactory);
BookFactory.inject = [];
function BookFactory() {
function newBook(entity) {
return {
author: entity[23],
publisher: entity[54],
};
}
return {
newBook: newBook
}
}
This way if that mapping ever changes, you will only have to edit this one function.
In your controller you can create a new book object like this:
var myBook = BookFactory.newBook(entity);
console.log(myBook.author); // this will work
As far as your error, we would need to see a lot more info to understand why you are receiving that. It is a very vague error, and it is unlikely that how you structure your book object has anything to do with it.
Pre-loading dependent promises in Angular 1
not sure how I am supposed to move the definition out of the function?
To get something "out of a function", use a return statement.
In the case of the .then method of a promise, what one gets "out of the function" is a new promise that resolves to what is returned. One can thus chain promises:
$routeProvider
.when('/Book/:bookId', {
templateUrl: 'book.html',
controller: 'BookController',
resolve: {
book: function($routeParams, BookService) {
var bookId = $routeParams.bookId;
var bookPromise = BookService.getBook(bookId);
var finalPromise = bookPromise.then( function(book) {
var authorPromise = BookService.getAuthor(book.authorId);
var nestedPromise = authorPromise.then( function(author) {
book.author = author;
return book;
});
return nestedPromise;
});
return finalPromise;
})
}
})
The resolve function does two service calls, the first looks up the book using the bookID, and the second looks up the author using the authorId returned by the first service call. The resolver adds the resolved author property to the book object. The chained promise resolves to the book object with its author property set to the author string.
Chaining Promises
Because calling the .then method of a promise returns a new derived promise, it is easily possible to create a chain of promises.
It is possible to create chains of any length and since a promise can be resolved with another promise (which will defer its resolution further), it is possible to pause/defer resolution of the promises at any point in the chain. This makes it possible to implement powerful APIs.
— AngularJS $q Service API Reference - Chaining Promises

angularjs save changes after digest has finished

I think this might be quite common use-case with any angular app. I am simply watching some objects on my scope that are changed as part of several digest cycles. After digesting them (changing their values via databinding) has finished, I want to save them to databse.
A. Now, with the current solutions I see following problems:
running save in $timeout() - how to assure that save is called only
once
running a custom function in $scope.$evalAsync - how to find out what has been chaged
There are of course solutions to both of these prolblems, but non of those I know seem ehough elegant to me.
The question is: What is the most elegant solution to the problem?
B. In particular, what are the best practices to
make sure that save gets called only once in a digest cycle
find out that object is dirty after last digest
Here is a solution I've found working best for me - as an AMD modul. Inspired by Underscore.
/**
* Service function that helps to avoid multiple calls
* of a function (typically save()) during angular digest process.
* $apply will be called after original function returns;
*/
define(['app'], function (app) {
app.factory('debounce', ['$timeout', function ($timeout) {
return function(fn){ // debounce fn
var nthCall = 0;
return function(){ // intercepting fn
var that = this;
var argz = arguments;
nthCall++;
var later = (function(version){
return function(){
if (version === nthCall){
return fn.apply(that, argz);
}
};
})(nthCall);
return $timeout(later,0, true);
};
};
}]);
});
/*************************/
//Use it like this:
$scope.$watch('order', function(newOrder){
$scope.orderRules.apply(newOrder); // changing properties on order
}, true);
$scope.$watch('order.valid', function(newOrder){
$scope.save(newOrder); //will be called multiple times while digested by angular
});
$scope.save = debounce(function(order){
// POST your order here ...$http....
// debounce() will make sure save() will be called only once
});

Qunit + Sinon to test Backbone's model events

These days I'm trying to put up some tests for my first serious Backbone app. I had no problem so far with normal test but now I'm stuck trying to setting up an async test.
Basically my server API return a page with a 500 HTTP code error if I try to save a model with invalid attributes and I want to check if this trigger the right "error" state in Backbone.
I've tried to set-up the test in this way:
asyncTest("Test save Model function", function(){
expect(1);
var user = new User({});
var err_spy = this.spy();
user.on('error',err_spy);
user.save(user,{error:function(){
start();
equal( err_spy.callCount, 1, "Callback 'error' called once");
}});
});
The problem is that the error callback of the save function overrides the one in the model, so the only way to trigger it would be to do it manually:
user.trigger("error");
I don't think it is a right way to test because in my production environment there is no error callback for model's save function, but on the other hand I don't know how to tell Qunit to wait the ajax response to evaluate the test assertion.
Can someone suggest me a way to make it work? Thank you!
Something like this should do the trick. I'm going from memory here, but the sinon fake server should allow you to immediately return the 500 error state and subsequently invoke the spied-on function. You might need to tweak the server.respondWith(...) call.
asyncTest("Test save Model function", function(){
expect(1);
var user = new User({});
// Set up a fake 500 response.
var server = sinon.fakeServer.create();
server.respondWith(500, {}, "");
// Create the error callback.
var err_callback = function(){};
var err_spy = sinon.spy(err_callback);
user.save(user, {error: err_callback});
server.respond();
equal( err_spy.callCount, 1, "Callback 'error' called once");
server.restore();
});

Wait for the collection to fetch everything in backbone

I have two set of collections. One is for the categories and the other is for the Items. I ned to wait for the categories to finish fetching everything for me to set the category for the Items to be fetched.
Also i everytime i click a category i must re-fetch a new Items Collection because i have a pagination going on everytime i click on a category it doesn't refresh or re-fetch the collection so the pagination code is messing with the wrong collection. Any ideas?
this.categoryCollection = new CategoryCollection();
this.categoryCollection.fetch();
this.itemCollection = new ItemCollection();
this.itemCollection.fetch();
Just ran into a similar situation. I ended up passing jquery.ajax parameters to the fetch() call. You can make the first fetch synchronous. From the backbone docs:
jQuery.ajax options can also be passed directly as fetch options
Your code could be simplified to something like:
this.categoryCollection.fetch({async:false});
this.itemCollection.fetch();
One quick way would be to just pass a callback into the first fetch() call that invokes the second. fetch() takes an options object that supports a success (and error) callback.
var self = this;
this.categoryCollection = new CategoryCollection();
this.categoryCollection.fetch({
success: function () {
self.itemCollection = new ItemCollection();
self.itemCollection.fetch();
}
});
Not the most elegant, but it works. You could probably do some creative stuff with deferreds since fetch() returns the jQuery deferred that gets created by the $.ajax call that happens.
For the pagination issue, it's difficult to tell without seeing what your pagination code is doing. You're going to have to roll the pagination stuff yourself since Backbone doesn't support it natively. What I'd probably do is create a new Collection for the page criteria that are being queried and probably create a server action I could hit that would support the pagination (mapping the Collection's url to the paginated server action). I haven't put a ton of thought into that, though.
I had to react to this thread because of the answers there.
This is ONLY WAY OF DOING THIS RIGHT!!!
this.categoryCollection = new CategoryCollection();
this.itemCollection = new ItemCollection();
var d1 = this.categoryCollection.fetch();
var d2 = this.itemCollection.fetch();
jQuery.when(d1, d2).done(function () {
// moment when both collections are populated
alert('YOUR COLLECTIONS ARE LOADED :)');
});
By doing that you are fetching both collections at same time and you can have event when both are ready. So you don't wait to finish loading first collections in order to fetch other, you are not making ajax calls sync etc that you can see in other answers!
Here is a doc on Deferred objects.
Note: in this example case when one or more deferred object fails it's not covered. If you want to cover that case also beside .done you will have to add .fail callback on .when and also add error handler that will mark failed d1 or d2 in this example.
I am using RelationalModel and I created a queued fetch, that only calls the 'change' event when done loading:
var MySuperClass = Backbone.RelationalModel.extend({
//...
_fetchQueue : [],
fetchQueueingChange : function(name){
//Add property to the queue
this._fetchQueue.push(name);
var me = this;
//On fetch finished, remove it
var oncomplete = function(model, response){
//From _fetchQueue remove 'name'
var i = me._fetchQueue.indexOf(name);
me._fetchQueue.splice(i, 1);
//If done, trigger change
if (me._fetchQueue.length == 0){
me.trigger('change');
}
};
this.get(name).fetch({
success: oncomplete,
error : oncomplete
});
},
//...
});
The class would call:
this.fetchQueueingChange('categories');
this.fetchQueueingChange('items');
I hope you can improve on this, it worked well for me.
I ended up with the same problem today and figured out a solution to this:
var self = this;
this.collection = new WineCollection();
this.collection.url = ApiConfig.winetards.getWineList;
this.collection.on("reset", function(){self.render()});
this.collection.fetch({reset: true});
Now when the fetch on the collection is complete a "reset" is triggered and upon "reset" call the render() method for the view.
Using {async: false} is not the ideal way to deal with Backbone's fetch().
just set jQuery to become synchronous
$.ajaxSetup({
async: false
});
this.categoryCollection.fetch();
this.itemCollection.fetch();
$.ajaxSetup({
async: true
});
This is the simplest solution, I guess. Of course, starting new requests while these fetches run will be started as synchronous too, which might be something you don't like.

BackboneJS test with jasmine if parameters are passed to fetch

I have a Backbone Collection and I need to pass some parameters to the .fetch({data : {gender : 'male'}}). Is there a way to test with jasmine that the parameters are passed?
thanks in advance
You could probably achieve that by using the method in the example below.
Please note that, the below code will intercept the fetch-call and return, the call will not reach the server. If you would like to have the server-side emulation, then you would need to use Sinon or some other such similar methods.
describe("People collection" function() {
var people = Backbone.Collection.extend({
// ...
});
function searchPeople(people, data ) {
people.fetch(data);
}
it("must verify the fetch parameters!", function(){
var param = {data : {gender : 'male'}};
// Set up the spy.
spyOn(people, 'fetch').andReturn(); // Warning: this makes the call synchronous, Fetch actually won't go through!
// Now perform the operation that would invoke Collection.fetch.
searchPeople(people, param);
expect(people.fetch).toHaveBeenCalled(); // Verifies the fetch was actually called.
expect(people.fetch).toHaveBeenCalledWith(param); // Verifies that the fetch was called with specified param.
});
});

Resources