How am I able to test that a Backbone model's method bound to the event bus has fired when using Karma and Sinon? - backbone.js

When testing that a backbone model's event has fired with a sinon spy, it erroneously errors: expected doSomething to be called once but was called 0 times, even though it seems to execute when a console log is put in the method's body. The testing function looks like:
it('Y U NO WORK', function() {
const events = {};
_.extend(events, Backbone.Events);
const Model = Backbone.Model.extend({
initialize: function() {
this.listenTo(events, 'doSomething', this.doSomething);
},
doSomething: function() {},
});
const model = new Model();
const spy = sinon.spy(model, 'doSomething');
events.trigger('doSomething');
sinon.assert.calledOnce(spy);
});
I know that to fix, you'd have to put the sinon spy on the Model's prototype like const spy = sinon.spy(Model.prototype, 'doSomething'); in the line before the new Model() call, however it seems to work without issue when put in the model instance, like below:
it('And this does work', function() {
const Model = Backbone.Model.extend();
const model = new Model();
const spy = sinon.spy(model, 'set');
model.set('foo', 'bar');
sinon.assert.calledOnce(spy);
});
Curious why it needs to be put on the model's prototype in the first instance, but works on the model instance in the second?

Spy kind of replaces the original method with a custom one to know when it's invoked (It's holds reference to original for restoring later). So in the first case you set up an event listener before creating the spy. The event system is actually holding direct reference to the original method, not to the spy. Spy can do nothing about it, Spy wouldn't know when it is invoked.
You need to set up the spy first before setting up the event listener, something like:
it('Y U NO WORK', function() {
var spy;
const events = {};
_.extend(events, Backbone.Events);
const Model = Backbone.Model.extend({
initialize: function() {
spy = sinon.spy(this, 'doSomething');
this.listenTo(events, 'doSomething', this.doSomething);
//now same as this.listenTo(events, 'doSomething', spy);
},
doSomething: function() {},
});
const model = new Model();
events.trigger('doSomething');
sinon.assert.calledOnce(spy);
});
Or avoid keeping direct references to the original method like:
this.listenTo(events, 'doSomething', function() {
//by the time this is invoked, original has been replaced with spy
this.doSomething();
});
It'd work because it's not holding reference to original method, the method invokation is dynamic

Related

AngularJS 1.6.9 controller variable bound to service variable doesn't change

I have 2 components which are both accessing a service. One component delivers an object and the other one is supposed to display it or just receive it. The problem is that after the initialization process is finished the variable in the display component doesn't change.
I have tried using $scope , $scope.$apply(), this.$onChanges aswell as $scope.$watch to keep track of the variable, but it always stays the same.
This controller from the display component provides a text, which is from an input field, in an object.
app.controller("Test2Controller", function ($log, TestService) {
this.click = function () {
let that = this;
TestService.changeText({"text": that.text});
}
});
That is the the service, which gets the objekt and saves it into this.currentText.
app.service("TestService", function ($log) {
this.currentText = {};
this.changeText = function (obj) {
this.currentText = obj;
$log.debug(this.currentText);
};
this.getCurrentText = function () {
return this.currentText;
};
});
This is the controller which is supposed to then display the object, but even fails to update the this.text variable.
app.controller("TestController", function (TestService, $timeout, $log) {
let that = this;
this.$onInit = function () {
this.text = TestService.getCurrentText();
//debugging
this.update();
};
//debugging
this.update = function() {
$timeout(function () {
$log.debug(that.text);
that.update();
}, 1000);
}
//debugging
this.$onChanges = function (obj) {
$log.debug(obj);
}
});
I spent quite some time searching for an answer, but most are related to directives or didn't work in my case, such as one solution to put the object into another object. I figured that I could use $broadcast and $on but I have heard to avoid using it. The angular version I am using is: 1.6.9
I see a problem with your approach. You're trying to share the single reference of an object. You want to share object reference once and want to reflect it wherever it has been used. But as per changeText method, you're setting up new reference to currentText service property which is wrong.
Rather I'd suggest you just use single reference of an object throughout and it will take care of sharing object between multiple controllers.
Service
app.service("TestService", function ($log) {
var currentText = {}; // private variable
// Passing text property explicitly, and changing that property only
this.changeText = function (text) {
currentText.text = text; // updating property, not changing reference of an object
$log.debug(currentText);
};
this.getCurrentText = function () {
return currentText;
};
});
Now from changeText method just pass on text that needs to be changed to, not an new object.

Spying on function of object created within Backbone view

I'm trying to spy on a Backbone view. Within one of the view function, an object is created and attached to the view:
var BackboneView = Backbone.View.extend({
anObject: null,
.....
viewFunction: function() {
if (!this.anObject) {
this.anObject = new theObject();
this.anObject.setTimeout(10);
}
}),
....
});
return BackboneView;
The object looks like this (and cannot be changed):
var theObject = function () {
this.setTimeout = function (timeout) {
//set timeout
};
return this;
});
return theObject;
I want to make sure setTimeout was called. So I tried:
it('theObject.prototype.setTimeout called', function () {
spyOn(theObject.prototype, 'setTimeout');
myBackboneView.viewFunction()
expect(theObject.prototype.setTimeout).toHaveBeenCalled();
});
But I get the error:
setTimeout() method does not exist
So I try to spy on it through the Backbone view:
it('myBackboneView.anObject.setTimeout called', function () {
spyOn(myBackboneView.anObject, 'setTimeout');
myBackboneView.viewFunction();
expect(myBackboneView.anObject.setTimeout).toHaveBeenCalled();
});
But I predictably get the error:
spyOn could not find an object to spy upon for setTimeout()
Any ideas how I make sure setTimeout() was called?
setTimeout() method does not exist
Because you did not set setTimeout on the function prototype.
if you want a function on a prototype you need to put it on a prototype,then you can spy on the function.
theObject.prototype.setTimeout=function(){/*some code*/}
and no need to return this in the constructor.
this.setTimeout = function (timeout) {
//set timeout
};
return this; // this is wrong!!!

Detect when Backbone has finished a model fetch

I can't seem to figure out which event to listen to when fetching data for a model. Usually when I'm doing it for a collection, I listen to the sync event. However, it seems like that doesn't work for models.
So, how do I know when my model is done fetching? Which event does it trigger?
Edit: Here's the beginning part of my view that is using the model:
var HomeContent = BaseView.extend({
initialize: function(options) {
self = this;
this.academyID = this.options.parent.academyID;
this.model = new AcademyModel({academyID: this.academyID});
this.model.on('sync', function() {
console.log('sync');
});
this.model.fetch();
}
fetch returns a jQuery promise. Just use something like:
this.model.fetch().done(function() {
...
}
Another solution is in the docs:
Accepts success and error callbacks in the options hash, which are both passed (model,response, options) as arguments.

Sinon spy method that is called in backbone view initialize

I execute a method in the Backbone's View initialize method.
initialize : function(options) {
this.myMethod();
}
I am trying to spy on this method using sinon like:
this.spyMyMethod = sinon.spy(this.view, "myMethod");
end then
it('should call my method', function(){
expect(this.spyMyMethod).toHaveBeenCalledOnce();
});
but the test fails...
Any ideas?
You are spying on the method too late.
Wherever you are assigning this.view I assume it is from a call like new Views.SomeView(). It is that new call that will make the initialize function be executed.
Update
I don't really recommend doing this because it is pretty messy, but you can possibly do something like the following: (I don't know sinon but this is how you would do it with the base jasmine spy objects)
it('should call my method', function(){
var dummyView = new Views.SomeView();
spyOn(dummyView, "myMethod");
spyOn(Views, "SomeView").andCallFake(function () {
dummyView.initialize();
return dummyView;
});
new Views.SomeView();
expect(dummyView.myMethod).toHaveBeenCalled();
});
Another Possiblilty
Looks like it might be possible to override that method with a spy like below. If that works, it is probably the cleanest way to do this.
it('should call my method', function(){
spyOn(Views.SomeView.prototype, "myMethod");
new Views.SomeView();
expect(Views.SomeView.prototype.myMethod).toHaveBeenCalled();
});
you need to return a new instance of your view for the initialize method to be called.
I'm not sure if this.view = new View(); already however

The best way to trigger an event when the model.fetch is complete

I am looking for the best way to trigger an event when the fetch is completed.
This code works but I am curious to know if there is a better way to accomplish the following task.
var myApp = new Marionette.Application();
myApp.on("initialize:before", function () {
this.currentUser = new UserModel();
this.currentUser.fetch({
complete: function () {
myApp.vent.trigger('currentUser');
}
});
});
A successful fetch triggers a "change" event:
fetch model.fetch([options])
[...] A "change" event will be triggered if the server's state differs from the current attributes.
So if the fetch does anything, there will be a "change" that you can listen for:
myApp.on("initialize:before", function () {
this.currentUser = new UserModel();
this.currentUser.on('change', function() {
myApp.vent.trigger('currentUser');
});
this.currentUser.fetch();
});
That will also trigger a "currentUser" event if this.currentUser is changed in some other way and that might or might not be what you want. If you only want your function called once then your current success handler is probably the easiest thing to do; you could still use on and unbind the handler when it is called:
myApp.on("initialize:before", function () {
var triggerCurrentUser = _.bind(function() {
myApp.vent.trigger('currentUser');
this.currentUser.off('change', triggerCurrentUser);
}, this);
this.currentUser = new UserModel();
this.currentUser.on('change', triggerCurrentUser);
this.currentUser.fetch();
});
You could also use _.once but that would leave a no-op callback function kicking around and there's no need for that.

Resources