When i created this question, my doubt was about how would i be able to test an asynchronous request utilizing mocha/enzyme/chai/sinon.
I am sure that there are different ways, but a possible one is to mock it with a handmade function that returns the appropriate values (check the answer for details).
My getInitialState method is this:
getInitialState: function() {
var me = this;
var documentData = null;
var promise = me.getDocuments();
promise.then(function(value) {
var documents = value.map(function(obj) {
return Object.keys(obj).sort().map(function(key) {
return obj[key];
});
});
documentData = documents;
});
return ({
cd: false
});
},
And the getDocuments() function that returns a promise is this:
getDocuments: function() {
var deferred = when.defer();
Collection.fetch({cd: workspaceInfo.getCD()}).then(
function(results) {
deferred.resolve(results);
},
deferred.reject
);
return deferred.promise;
},
How can i successfuly test it?
Would i have to mock the getInitialState method itself? (is that even possible)
Or just the getDocuments function with some predictable return values?
Thanks in advance, any help will be appreciated.
I solved this by requiring the Collection (which is a rest API that brings some values from a database)
var Collection = require("path/to/my/collection/Collection");
Afterwards, i use it in my getDefaultProps() method:
getDefaultProps() {
return {
Collection: new Collection()
};
},
And this in turn enables me to write tests that initialize a mocked Collection (fileDataResponse is a JSON with some data):
var CollectionMock= {
fetch: () => {
return {
then: callback => callback(fileDataResponse)
};
}
};
And use it in my test afterwards:
it("should open the modal without a loaded configuration", function() {
var instance, wrapper;
wrapper = mount(
<DocumentPreview
Collection={CollectionMock}/>
);
instance = wrapper.component.getInstance();
instance.openModal();
expect(wrapper.find('#MockedTest' + 'docOid251085').exists()).to.equal(true);
wrapper.unmount();
});
I have a model where I want to add a parse method in order to do extra data work (setting up a moment.js object for date fields).
But the function is never called (both model and collection).
collection :
class SomethingCollection extends Parse.Collection
model: SomethingModel
parse: ->
console.log('parse from collection')
model :
class SomethingModel extends Parse.Object
className: 'Something'
parse: ->
console.log('parse from model')
from a view :
#collection = new SomethingCollection()
#listenTo( #collection, 'add', -> console.log('fire add event') )
#collection.fetch(parse: true, silent: false, add: true)
EDIT :
It seems to happen in the Parse.Query.find callback, see below code comments.
So it cannot be done in the initialize method as well, but where else ? I suspect Parse.Object to be not so similar with Bakbone.Model
find: function(options) {
var self = this;
options = options || {};
var request = Parse._request({
route: "classes",
className: this.className,
method: "GET",
useMasterKey: options.useMasterKey,
data: this.toJSON()
});
return request.then(function(response) {
return _.map(response.results, function(json) {
var obj;
if (response.className) {
obj = new Parse.Object(response.className); // <- no attributes or options used, blank object
} else {
obj = new self.objectClass(); // <- no attributes or options used, blank object
}
obj._finishFetch(json, true); // <- magically do things out of any constructor or parse function
return obj;
});
})._thenRunCallbacks(options);
},
I found no other way than to redeclare that cursed _finishFetch method :
original_finishFetch = _(Parse.Object.prototype._finishFetch)
Parse.Object.prototype._finishFetch = (serverData, hasData) ->
original_finishFetch.bind(#)(#parse(serverData), hasData)
That way the data is processed in the parse method, as it is expected to be with any Backbone Model, or any sdk which implements the Backbone Model interface.
I am currently writing tests for some services using karma with jasmine, and I was wondering if I had to mock a service's service dependency that uses $http, as described below.
PS: I'm already using $httpBackend to mock any GET request, and I plan on using $httpBackend.expect* if I don't mock the service ApiProvider
This is the service I am testing
.factory('CRUDService', ['ApiProvider', function (ApiProvider) {
'use strict';
var CRUD = function CRUD(modelName) {
this.getModelName = function () {
return modelName;
};
},
overridableMethods = {
save: null
};
CRUD.prototype = {
save: function () {
// ABSTRACT
},
/**
* Deletes instance from id property
* #return http promise
*/
remove: function () {
return ApiProvider.delete(this.getModelName(), this.id);
}
};
return {
/**
* Function creating a class extending CRUD
* #param {string} modelName
* #param {Object} methods an object with methods to override, ex: save
* return {classD} the extended class
*/
build: function (modelName, methods) {
var key,
Model = function () {
};
// Class extending CRUD
Model.prototype = new CRUD(modelName);
// Apply override on methods allowed for override
for (key in methods) {
if (key in overridableMethods &&
typeof methods[key] === 'function') {
Model.prototype[key] = methods[key];
}
}
/**
* Static method
* Gets an entity of a model
* #param {Object} config #see ApiProvider config
* #return {CRUD} the reference to the entity
*/
Model.get = function (config, success) {
var entity = new Model();
ApiProvider.get(modelName, config)
.success(function (data) {
angular.extend(entity, data);
if (success) {
success();
}
});
return entity;
};
/**
* Static method
* Gets entities of a model
* #param {Object} config #see ApiProvider config
* #return {CRUD[]} the reference to the entity
*/
Model.query = function (config, success) {
var entities = [];
ApiProvider.get(modelName, config)
.success(function (data) {
data.map(function (model) {
var entity = new Model();
angular.extend(entity, model);
return entity;
});
Array.prototype.push.apply(entities, data);
if (success) {
success();
}
});
return entities;
};
return Model;
},
// Direct link to ApiProvider.post method
post: ApiProvider.post,
// Direct link to ApiProvider.put method
put: ApiProvider.put
};
}]);
And this is the service's service dependency ApiProvider
.service('ApiProvider', function ($http) {
/**
* Private
* #param {string}
* #param {object}
* #return {string} Example: /service/[config.id[/config.relatedModel], /?config.params.key1=config.params.value1&config.params.key2=config.params.value2]
*/
var buildUrl = function (service, config) {
var push = Array.prototype.push,
url = [apiRoot, service],
params = [],
param = null;
// if a key id is defined, we want to target a specific resource
if ('id' in config) {
push.apply(url, ['/', config.id]);
// a related model might be defined for this specific resource
if ('relatedModel' in config) {
push.apply(url, ['/', config.relatedModel]);
}
}
// Build query string parameters
// Please note that you can use both an array or a string for each param
// Example as an array:
// {
// queryString: {
// fields: ['field1', 'field2']
// }
// }
// Example as a string
// {
// queryString: {
// fields: 'field1,field2'
// }
// }
if ('queryString' in config) {
// loop through each key in config.params
for (paramName in config.queryString) {
// this gives us something like [my_key]=[my_value]
// and we push that string in params array
push.call(params, [paramName, '=', config.queryString[paramName]].join(''));
}
// now that all params are in an array we glue it to separate them
// so that it looks like
// ?[my_first_key]=[my_first_value]&[my_second_key]=[my_second_value]
push.apply(url, ['?', params.join('&')]);
}
return url.join('');
},
request = function (method, url, methodSpecificArgs) {
trace({
method: method,
url: url,
methodSpecificArgs: methodSpecificArgs
}, 'ApiProvider request');
return $http[method].apply($http, [url].concat(methodSpecificArgs));
},
methods = {
'get': function (url, config) {
config.cache = false;
return request('get', url, [config]);
},
'post': function (url, data, config) {
config.cache = false;
return request('post', url, [data, config]);
},
'put': function (url, data, config) {
config.cache = false;
return request('put', url, [data, config]);
},
'delete': function (url, config) {
config.cache = false;
return request('delete', url, [config]);
}
};
return {
'get': function (service, config) {
config = config || {};
return methods.get(buildUrl(service, config), config);
},
'post': function (service, data, config) {
config = config || {};
return methods.post(buildUrl(service, config), data, config);
},
'put': function (service, data, config) {
config = config || {};
return methods.put(buildUrl(service, config), data, config);
},
'delete': function (service, config) {
config = config || {};
return methods.delete(buildUrl(service, config), config);
}
};
});
Finally this is how I tested the CRUDService so far
describe('CRUDServiceTest', function () {
'use strict';
var CRUDService;
beforeEach(function () {
inject(function ($injector) {
CRUDService = $injector.get('CRUDService');
});
});
it('should have a method build', function () {
expect(CRUDService).toHaveMethod('build');
});
it('should ensure that an instance of a built service has a correct value for getModelName method',
function () {
var expectedModelName = 'myService',
BuiltService = CRUDService.build(expectedModelName),
instanceOfBuiltService = new BuiltService();
expect(instanceOfBuiltService).toHaveMethod('getModelName');
expect(instanceOfBuiltService.getModelName()).toEqual(expectedModelName);
});
// TEST get
it('should ensure build returns a class with static method get', function () {
expect(CRUDService.build()).toHaveMethod('get');
});
it('should ensure get returns an instance of CRUD', function() {
var BuiltService = CRUDService.build(),
instanceOfBuiltService = new BuiltService();
expect((BuiltService.get()).constructor).toBe(instanceOfBuiltService.constructor);
});
// TEST query
it('should ensure build returns a class with static method query', function () {
expect(CRUDService.build()).toHaveMethod('query');
});
it('should a collection of CRUD', function () {
expect(CRUDService.build()).toHaveMethod('query');
});
it('should have a static method post', function () {
expect(CRUDService).toHaveMethod('post');
});
it('should have a static method put', function () {
expect(CRUDService).toHaveMethod('put');
});
});
TLDR;
To mock or not to mock a depending service depending itself on $http ?
In general I think it's a good idea to mock out your services. If you keep up on doing it, then it makes isolating the behavior of any service you add really easy.
That being said, you don't have to at all, you can simply use Jasmine spy's.
for instance if you were testing your CRUDService which had a method like this:
remove: function () {
return ApiProvider.delete(this.getModelName(), this.id);
}
You could, in your test write something like:
var spy = spyOn(ApiProvider, 'delete').andCallFake(function(model, id) {
var def = $q.defer();
$timeout(function() { def.resolve('something'); }, 1000)
return def.promise;
});
Then if you called it:
var promise = CRUDService.remove();
expect(ApiProvider.delete).toHaveBeenCalledWith(CRUDService.getModelName(), CRUDService.id);
So basically you can mock out the functionality you need in your test, without fully mocking out the service. You can read about it more here
Hope this helped!
In Backbonejs, is it possible to initialize event handlers in a loop even when the handlers make use of the functions in the outer model?
For e.g. how can I get something like the below to work?
var MyModel = Backbone.Model.extend({
eventHandlers: {
onerror: function(e) {
this.myErrorHandler(e); // doesnt work
},
},
initialize: function () {
var myObj = {}
_.each(eventHandlers, function(value, key) {
myObj[key] = value;
}, this);
},
myErrorHandler: function(e) {
console.error('my error handler', e);
}
});
How do I get the Model's error handler function (myErrorHandler) to be called from the event handlers that are declared in a sub object? Or is there some other way to achieve this?
TIA
Do you mean to execute each handler on event defined by its key, bound to the model instance?
var MyModel = Backbone.Model.extend({
eventHandlers: {
error: function(e) {
this.myErrorHandler(e); // doesnt work
}
},
initialize: function () {
var _this = this;
_.each(this.eventHandlers, function(value, key) {
_this.on(key, _(value).bind(_this));
});
},
myErrorHandler: function(e) {
console.error('my error handler', e);
}
});
But what if you want to have several handlers for the same event?
exports.definition = {
config : {
// table schema and adapter information
},
extendModel: function(Model) {
_.extend(Model.prototype, {
// Extend, override or implement the Backbone.Model methods
});
return Model;
},
extendCollection: function(Collection) {
_.extend(Collection.prototype, {
}); // end extend
return Collection;
}
}
When i try to do var model = Alloy.createCollection('model');
alert(appointments.fetch());
I don't get any result.
fetch is asynchronous. As so, it will return a promise, or you need to set up a callback:
appointments.fetch().done(function( r ) { console.log(r) });
Also, never use alert to debug; this is useless. Always open your console and use console.log