I'm trying to get my head around testing a parse.com backed data service, but have not found a satisfying solution so far. Basically, I want to use karma/jasmine to test a function returning a promise.
I'm spying on the query.get() method to intercept and return an object that was created in the implementation of the jasmine spec. However, when the spy returns the local object, the success path of the query.get options argument is not executed. Instead, the to-be-tested function directly returns the unresolved promise, which is not what I want. Basically, I want to test the success path (and the error path) of the data service.
Here's the code:
to-be-tested parse.com data services
angular.module('app.services',[])
.factory('AppServices',function(){
var doFunction1 = function(){
var promise = new Parse.Promise();
var ParseObjectClass = Parse.Object.extend('ParseObject');
var query = new Parse.Query(ParseObjectClass);
query.get('abc1234',{
success: function(result){
promise.resolve(result);
},
error: function(result, error){
promise.reject(error);
}
});
return promise;
}
return {
function1: doFunction1
}
});
jasmine spec
describe('MyServices Tests', function() {
var AppServices;
var scope;
beforeEach(function() {
module('app.services');
});
beforeEach(inject(function($rootScope, AppServices) {
AppServices = AppServices;
scope = $rootScope.$new();
Parse.initialize("key1", "key2");
var user = new Parse.User({
id: 'abc1234',
});
var ParseObjectClass = Parse.Object.extend('ParseObject');
var obj1 = new ParseObjectClass({
id: 'xyc789'
});
spyOn(Parse.Query.prototype, 'get')
.and.callFake(function(options) {
return Parse.Promise.as(obj1);
});
}));
it('parseMockTest', function() {
var result = AppServices.function1(2);
console.log(JSON.stringify(result));
});
});
This is the result from the karma log:
LOG: '{"_resolved":false,"_rejected":false,"_resolvedCallbacks":[],"_rejectedCallbacks":[]}'
I would have expected that the returned promise is resolved.
Any ideas/hints on how to test functions that return promises?
Related
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();
});
Have tried everything I've found on the internet to make this work with no success. Trying to test a function in my service, but according to my coverage I'm never accessing it. Any help would be greatly appreciated :)
Service:
'use strict';
angular.module('Service').service('configService', function(
$rootScope, $http) {
var configObj = null;
return {
getConfig: function() {
if (configObj != null) {
console.log("returning cached config");
return configObj;
}
else {
return $http.get('conf.json').then(function(res) {
$http.get(res.confLocation).then(function(
locationResponse) {
configObj = locationResponse;
$rootScope.configObj = configObj;
console.log($rootScope.configObj);
return configObj;
});
});
}
}
};
});
getConfig is never being accessed in the tests I've tried.
ServiceTests:
'use strict';
describe('Service: configService', function() {
// load the controller's module
beforeEach(module('Service'));
var configService, $httpBackend, results, tstLocation, tstRes;
var tstConfig = {
"confLocation": "local-dev-conf.json"
};
var tstConfigEmpty = {};
var tstConfigObjEmpty = {};
var tstConfigObj = {
"AWS": {
"region": "us-east-1",
"endpoint": "http://localhost:8133"
}
};
// Initialize the controller and a mock scope
beforeEach(inject(function(_configService_, _$httpBackend_) {
inject(function($rootScope) {
$rootScope.USERNAME = 'TESTER';
$rootScope.configObj = tstConfigObj;
$rootScope.locationResponse = tstLocation;
$rootScope.res = tstRes;
});
configService = _configService_;
$httpBackend = _$httpBackend_;
//Problem here??
spyOn(configService, 'getConfig').and.callFake(function() {
return {
then: function() {
return "something";
}
};
});
}));
it('should return a promise', function() {
expect(configService.getConfig().then).toBeDefined();
});
it('should test backend stuff', inject(function() {
results = configService.getConfig(tstConfig);
$httpBackend.expectGET('conf.json').respond(tstConfig);
$httpBackend.expectGET('local-dev-conf.json').respond(tstConfigObj);
$httpBackend.flush();
}));
//Thanks Miles
it('should check if it was called', inject(function() {
results = configService.getConfig().then();
expect(configService.getConfig).toHaveBeenCalled();
});
// console.log(results);
}));
it('should check for a null configObj', inject(function() {
results = configService.getConfig(tstConfigObjEmpty).then(function() {
expect(results).toBe(null);
});
// console.log(results);
// console.log(tstConfigObj);
}));
it('should check for a non-null configObj', inject(function() {
results = configService.getConfig(tstConfigObj).then(function() {
// Any string is accepted right now -- Why??
expect(results).toEqual("returning cached config");
expect(results).toBe("returning cached config");
expect(results).toBe("your mom"); // SHOULDN'T BE WORKING BUT DOES
expect(results).toEqual("Object{AWS: Object{region: 'us-east-1', endpoint: 'http://localhost:8133'}}");
expect(results).toBe("Object{AWS: Object{region: 'us-east-1', endpoint: 'http://localhost:8133'}}");
});
// console.log(results);
// console.log(tstConfigObj);
}));
it('should check for null file', inject(function() {
results = configService.getConfig(tstConfigEmpty).then(function() {
expect(results).toEqual(null);
expect(results).toBe(null);
});
}));
it('should test a valid file', inject(function() {
results = configService.getConfig(tstConfig).then(function() {
expect(results).not.toBe(null);
expect(results).toEqual("Object{confLocation: 'local-dev-conf.json'}");
})
});
I think I'm using spyOn wrong, or not accessing getConfig in my tests properly. Thoughts?
EDIT: Here is my code coverage
EDIT 2: Changed test 3 thanks to a problem found by Miles, still no update on test coverage though. Something is wrong with my spyOn logic as Amy pointed out. I shouldn't be using callFake it seems?
EDIT 3: Got it accessing the function now thanks to Miles. Had to change my spyOn to:
spyOn(configService, 'getConfig').and.callThrough();
then add the test case:
results = configService.getConfig(tstConfig).then();
expect(configService.getConfig).toHaveBeenCalled();
Coverage now (still needs work)
You're calling a fake instead of the function. So the logic inside of the function does not get called.
You have an issue here:
results = configService.getConfig(tstConfigObj).then(function() {
expect(results).toHaveBeenCalled();
expect(results).toHaveBeenCalledWith(tstConfigObj);
});
getConfig takes no parameters, and neither does then. Omitting these errors, results is assigned the string "something" from then. Even if the expect statements fire, you seem to be testing if a string has been called. Try this instead:
results = configService.getConfig().then();
expect(configService.getConfig).toHaveBeenCalled();
What version of Jasmine are you using? The and.callFake syntax was added in Jasmine 2.0. Maybe the test suite just needs to point to the new version.
Jasmine 1.3 Docs
Jasmine 2.0 Docs
I'm trying to add an abort function to my promise in Angular. When debugging the code, I can see the function gets added as expected. However, when the object is returned to the calling service, the function is no longer there. I'm hoping it's something trivial.
.factory('MatchmakerSearch', ['$resource', 'OBB_ENV_CONF', '$q', function ($resource,
OBB_ENV_CONF, $q) {
// Create the $resource object to handle the API requests
function _query(params) {
var _deferredAbort = $q.defer();
var _request = $resource(OBB_ENV_CONF.API_HOST + 'int/matchMaker', {}, {
'query': {
method: 'GET',
params: params,
isArray: false,
timeout: _deferredAbort.promise
}
});
var _promise = _request.query().$promise.then( // Convert from $resource to $http
function (response) {
return response;
}, function (response) {
return $q.reject('Ajax call aborted');
}
);
_promise.abort = function () {
_deferredAbort.resolve();
};
_promise.finally(function () {
_promise.abort = angular.noop;
_deferredAbort = _request = _promise = null;
});
return _promise; // <~~~~ abort function exists here
}
return {
query: _query
}
}
]);
The service making the call looks like this:
_searchRequest = MatchmakerSearch.query(buildQueryParams()).then(function (result) {
// <~~~~ _searchRequest does not contain an abort() function.
});
I really thought this would be a simple thing to code. Any ideas on why my function is disappearing on return?
Every time you chain a promise with then, catch, or finally you receive a new Promise back:
_searchRequest = MatchmakerSearch
.query(buildQueryParams()) // Your customised promise.
.then(function (result) {}) // a new promise returned here.
So _searchRequest ends up being a fresh new Promise instance.
The documentation for deferred.then() talks about this:
then(successCallback, errorCallback, notifyCallback) – ...
This method returns a new promise which is resolved or rejected via the return value of the successCallback, errorCallback ...
(Emphasis in the original)
I have moved some common code to factory. but the controller is executing before factory get loaded. In this case i am getting the blank response(zero results)
can anyone suggest the best solution.
here is my angular factory,
app.factory('TabsFactory', function($resource){
var activetabs = {};
activetabs.getDepositAccountDetails = function() {
return $resource('xxxx/:number', {}, {
getDepositAccountDetailsService: {
method: 'GET',
isArray: false
}
});
}
activetabs.getAccountInfo = function(){
return accountinit.accountInfo;
}
activetabs.setAccountInfo = function(accountnumber, result) {
var accountinit = {
accountInfo: []
}
if (result.code == "v") {
activetabs.getDepositAccountDetails().getDepositAccountDetailsService({
number: accountnumber
}).$promise.then(function(response) {
accountinit.accountInfo = response;
//here i am getting the JSON response
}, function(error) {
});
}
return accountinit;
}
return activetabs;
});
controller,
TabsFactory.setAccountInfo(accountnumber, $scope.accountInfo);
$scope.accountInfo = TabsFactory.getAccountInfo();
alert(JSON.stringify($scope.accountInfo));
You should use chain promise to update scope variable, because your accountInfo variable is updated inside $resource promise.
Code
TabsFactory.setAccountInfo(accountnumber, $scope.accountInfo).then(function(data){
$scope.accountInfo = TabsFactory.getAccountInfo();
alert(JSON.stringify($scope.accountInfo));
});
Update
Service method should return promise inorder to continue promise chain
activetabs.setAccountInfo = function(accountnumber, result) {
var accountinit = {
accountInfo: []
}
if (result.code == "v") {
//added return below
return activetabs.getDepositAccountDetails().getDepositAccountDetailsService({
number: accountnumber
}).$promise.then(function(response) {
accountinit.accountInfo = response;
return accountinit.accountInfo;
//here i am getting the JSON response
}, function(error) {
});
}
return accountinit;
}
Yes, this will happen because of JavaScript executing asynchronous operations but your controller in such a way that it expects things to be synchronous operations.
When you call TabsFactory.getAccountInfo() its possible that your $resource('xxxx/:number') is still not completed and response ready for you to process!!
So, what to do? You have make use of promise. I usually have a repository (A factory with method that return promise) to handle server communications. Here is an example:
app.factory('accountRepository', ["$http","$q",function($http,$q){
return {
getDepositAccountDetails : function(id) {
var deferred = $q.defer();
$http.ger('xxx').success(deferred.resolve).error(deferred.reject);
return deferred.promise;
}
};
}] );
My repository will have more operations like add account, update account info etc..
my controller/service then calls these methods as follows:
accountRepository.getDepositAccountDetails(123).then(function(response) {
// Process the response..
}, function(error) {
// Some error occured! handle it
});
doing so, my code gets executed only after I get response from server and data is ready for consumption or display. Hope this helps..
Update: You might want to have a look at this to get the idea ;)
I have a factory, which I am attempting to unit test, with an injected PouchDB wrapper. The issue I am running into is that I have the mock PouchDB service returning a promise and while the promise resolves (I can console.log out the successCb and it looks correct), the karma expect statements within the successCb function where the correct console.log output is, are not running.
I am not sure why the console.log output works fine but the expect statements do not execute, any help on this matter would be greatly appreciated!
The following are the relevant snippets of code:
[ Unit Testing Setup ]
var mockCalendarsResource, mockPouchDB;
var mockPouchDB = function(name) {
this.name = name;
this.post = function(newCalendar, successCb, errorCb) {
var promise = new Promise(function(successCb, errorCb) {
newCalendar._id = 2;
successCb(newCalendar);
});
return promise;
};
}
beforeEach(function() {
module(function($provide) {
$provide.value('Pouch', mockPouchDB);
});
angular.mock.inject(function($injector) {
mockCalendarsResource = $injector.get('Calendar');
});
});
[ Karma Unit Test Code ]
describe('The calendar resource function create', function() {
it('should create a new calendar', inject(function(Calendar) {
var result = mockCalendarsResource.create({
name: "Testing"
},
function(response) {
//console.log here works correctly //
//These are the expect statements not functioning //
expect(response._id).toBe(2);
expect(response.name).toBe('Testing');
// Could put any expectation here and it will pass or not be checked //
}, function(err) {
});
}));
});
I believe you should be using Jasmine 2.0's done() to signal that the test has finished. If you don't then the async tests will not finish.