Error when testing Promise that contain .catch() methods - angularjs

I have an Angular1 Controller that calls a Service which returns a promise. When adding a .catch() method to the controllers call to the service, mocha throws the following error.
TypeError: undefined is not an object (evaluating 'DogService.getDogs(_this.userId)
.then(function(result){
_this.myDogs = result;
})
.catch') in app/scripts/controllers/main.js (line 20)
init#app/scripts/controllers/main.js:20:11
test/spec/controllers/main.js:33:20
loaded#http://localhost:8080/context.js:151:17
Controller
angular.module('testProblemApp').controller('MainCtrl', ['DogService', function (DogService) {
var _this = this;
_this.myDogs = [];
_this.userId = 1;
_this.init = function(){
DogService.getDogs(_this.userId)
.then(function(result){
_this.myDogs = result;
})
.catch(function(error){
console.log(error);
});
};
}]);
Test
describe('initialze function', function () {
it('should set the myDogs array to the value returned by the Service', function () {
spyOn(DogService, 'getDogs').and.callFake(function () {
return {
then: function (callback) {
return callback([{ id: 1, name: 'baxter' }]);
},
catch: function(callback){
return callback('Error');
}
}
});
MainCtrl.init();
expect(MainCtrl.myDogs).toEqual([{ id: 1, name: 'baxter' }]);
});
});
If I remove the .catch() from the controller the test passes.

The problem here is chaining. It is expected that then will return a promise object that has catch method. While then in getDogs mock returns undefined.
It is inconvenient to mock promises or other core features with custom stubs written from scratch. $q promises can be tested with $q promises:
var dogsPromiseMock;
...
spyOn(DogService, 'getDogs').and.callFake(function () {
return dogsPromiseMock;
});
...
dogsPromiseMock = $q.resolve([{ id: 1, name: 'baxter' }]);
MainCtrl.init();
$rootScope.$digest();
expect(MainCtrl.myDogs).toEqual(...);
...
dogsPromiseMock = $q.reject();
MainCtrl.init();
$rootScope.$digest();
expect(MainCtrl.myDogs).toEqual(...);
As a rule of thumb, it is preferable to mock services fully when testing controller units, not just mock single methods.

Related

How to I mock a service returning a promise in angularjs with done() and catch() callbacks

I want to mock a service for unit test in angularjs which looks something like this:
TranslationService.translate(args)
.then(function translated(value) {
//somecode
return;
})
.catch()
.done();
Following this answer:
How do I mock a service that returns promise in Angularjs Jasmine unit test?
This is what I did to mock it :
TranslateServiceMock = {
translate: jasmine.createSpy('translate').and.callFake(function() {
var deferred = $q.defer();
deferred.resolve('Remote call result');
return deferred.promise;
})};
But seems like this still doesn't work, I am guessing its because of the chained 'done' and 'catch'methods,
This is the error I get:
TypeError: undefined is not a constructor (near '....done();...'
Running out of ideas why this might be happening or how to fix this..
As mentioned in the comments, done is not a part of promise object.
I got this working by stubbing the done callback :
beforeEach( function () {
module(myModule.name, function ($provide) {
// define a .done on the $q Promise
$provide.decorator('$q', function ($delegate) {
var Promise = $delegate.when().constructor;
Promise.prototype.done = angular.noop;
return $delegate;
});
$provide.factory('TranslationService', function ($q) {
var svc = jasmine.createSpyObj('TranslationService', ['translate']);
svc.translate.and.returnValue($q.when(''));
return svc;
});
});
});

How to unit test condition in promise then() karma and jasmine

I am using AngularJS 1.7 with Karma and Jasmine. And I have started learning Unit Test cases.
I have a sample method below in my controller
_this.method = function () {
Service.getData().then(function (response) {
if (response.productId === "ClientAPI") {
// Some code
}
else {
// Some Code
}
}, function (error) {
_this.inProgress = false;
if (error.status === 400) {
// Some Code
} else {
// Some Code
}
})
}
Below is my test case :
describe('Some Route :: Controller => ', function () {
var $componentController;
var Service;
beforeEach(module('app'));
beforeEach(inject(function (_$componentController_, _Service_) {
Service = _Service_;
spyOn(Service, 'getData').and.callFake(function() {
var deferred = $q.defer();
var response = {};
response.productId = "ClientAPI";
deferred.resolve(result);
return deferred.promise;
});
ctrl = $componentController('controllerName', { Service: Service });
}));
it('Ctrl Method : should true', function () {
ctrl.method();
expect(Service.getData).toHaveBeenCalled();
Service.getData().then(function (response) {
expect(response.productId).toBe("ClientAPI")
})
});
});
But my branch coverage is not showing for this condition if (response.productId === "ClientAPI") {
Not sure what I am doing wrong while testing in a promise.
You need to call $scope.$apply() to trigger the call of the promise callbacks:
beforeEach(inject(function (_$componentController_, _Service_) {
Service = _Service_;
spyOn(Service, 'getData').and.returnValue($q.resolve({ productId: 'ClientAPI' }));
ctrl = $componentController('controllerName', { Service: Service });
}));
it('Ctrl Method : should true', inject(function($rootScope) {
ctrl.method();
expect(Service.getData).toHaveBeenCalled();
$rootScope.$apply();
// now test that the ctrl state has been changed as expected.
// testing that the service has returned ClientAPI is completely useless:
// the service is a mock, and you have told the mock to return that
// this should test the component, based on what you've told the service
// to return. It's not supposed to test the mock service.
// testing what the service returns tests jasmine, not your code.
});

testing .success and .error of service in controller jasmine

I have a call to service's function in a controller. Below is the code
Service
(function () {
'use strict';
angular.module('MyApp')
.service('MyService', ['$http', function ($http) {
return {
getMyData: function (extension) {
return $http.get('www.something.com');
}
};
}])
})();
Controller
var getMyData = function () {
MyService.getMyData(extension).success(function (results) {
//Some functionality here
})
.error(function (err, status) {
//Some functionality here
});
}
$scope.Call=function(){
getMyData();
}
$scope.Call();
Now please tell me how to mock the service call (may be with providers). How to test the above functions with complete code coverage.
My spec file:
$provide.service("MyService", function () {
this.getMyData= function () {
var result = {
success: function (callback) {
return callback({ ServerFileName: "myserverfilename"});
},
error: function (callback) {
return callback({ ServerFileName: "myserverfilename" });
}
};
return result;
}
//......
my controller initiation and other code
This code is not covering error block and giving the error
Cannot read property 'error' of undefined
Please help me how to write/mock the getMyData function of my service in my spec file
Thanks in advance.
Since .success and .error are old and have been replaced with .then(successCallback, errorCallback), you should consider replacing your chained .success and .error calls with a single call to the .then method with two callbacks as arguments to it: first being a success callback and second being an error callback.
If that's what you're willing to do, here's your working example:
You Module, Service and Controller
angular.module('MyApp', []);
angular.module('MyApp')
.service('MyService', ['$http', function ($http) {
return {
getMyData: function (extension) {
return $http.get('www.something.com');
}
};
}]);
angular.module('MyApp')
.controller('MyAppController', ['$scope', function($scope){
var extension = { foo: 'bar' };
var getMyData = function () {
MyService.getMyData(extension).then(function (results) {
//Some functionality here
}, function (err, status) {
//Some functionality here
});
}
$scope.Call=function(){
getMyData();
}
$scope.Call();
}]);
And your Test
describe('Controller: MyAppController', function(){
beforeEach(module('MyApp'));
var flag, extension, $q;
extension = { foo: "bar" };
beforeEach(inject(function($controller, $rootScope, _MyService_, _$q_) {
$scope = $rootScope.$new();
MyService = _MyService_;
$q = _$q_;
spyOn(MyService, 'getMyData').and.callFake(function(){
return flag ? $q.when(): $q.reject();
});
MyAppController = $controller('MyAppController', {
$scope: $scope,
MyService: MyService
});
}));
describe('function: Call', function() {
//Text for Success Callback
it('should implicitly call MyService.getMyData with an extension object', function() {
flag = true;
$scope.Call();
expect(MyService.getMyData).toHaveBeenCalledWith(extension);
});
//Text for Error Callback
it('should implicitly call MyService.getMyData with an extension object', function() {
flag = false;
$scope.Call();
expect(MyService.getMyData).toHaveBeenCalledWith(extension);
});
});
});
UPDATE:
I've tried making something like this to work but with no luck. Since .error()'s call is chained to .success() call, and that is something that will get called only after .success() has been called, it will never get to .error()'s call and we'll not be able to mock .error(). So if we try doing that, we'll always get an error like:
Cannot read property 'error' of undefined
So either you can use the comment /*istanbul ignore next*/ to skip this part in the coverage, or switch to .then().
Hope this helps.
You need to use spyon which would create some sort of mock for your service. You need to do this in your test file. Please check the below code:
spyOn(MyService, "getMyData").and.callFake(() => {
return {
error: (callback) => {
return callback({});
}
};
});
Hope i answered your question
Here is the solution. I had also encountered similar issue. Look like we have to design our own code and Jasmine allows us to design, customize the callback method. In chaining, return this object is mandate for Javascript method chaining. Using my solution you dont need to use then function
$provide.service("MyService", function () {
this.getMyData= function () {
var result = {
success: function (callback) {
callback({ ServerFileName: "myserverfilename"});
//returning main object for error callback invoke to occur
return this;
},
error: function (callback) {
callback({ ServerFileName: "myserverfilename" });
//returning this object will initialize error callback with object since you are chaining
return this;
}
};
return result;
}

Integration Testing AngularJS + Karma + Jasmine

I would like to test my angular service I would like to test it with real data - a.k.a (Integration Test). I'm using Jasmine and Karma.
Here is my test:
describe('Trending Data Service', function () {
var value = 0, originalTimeout = 0;
var service, Enums, $httpBackend;
// initialize module
beforeEach(module('waterfall'));
// initialize services
beforeEach(inject(function ($injector) {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
$httpBackend = $injector.get('$httpBackend');
service = $injector.get("trendingService");
Enums = $injector.get("Enums");
spyOn(service, 'fetch').and.callThrough();
}));
it('check if dependencies are defined', function () {
expect(service).toBeDefined();
expect(Enums).toBeDefined();
expect(service.categories).toBeDefined();
expect(service.fetch).toBeDefined();
});
it('categories array should be defined within the service', function () {
expect(service.categories.length).toEqual(9);
expect(service.categories).toEqual(jasmine.any(Array));
});
// this test is alway fails...
it('fetch method should return initial result', function (done) {
var promise = service.fetch(Enums.socials.viewAll, false);
promise.then(function (result) {
done();
}, function() {
expect(1).toBe(2);
done.fail('Error occured');
});
});
}
This is the error:
Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
I tried a wide range of solutions and I haven't got any success with this.
EDIT: 29/April/2016
var trendingService = function ($q, $http) {
var deferred = $q.defer();
var $this = this;
this.fetch = function (id) {
$http.get(url).then(function (result) {
deferred.resolve(result);
}).catch(function(err) {
deferred.reject(err);
});
return deferred.promise;
}
return {
fetch: $this.fetch;
};
};
var Enums = {
Roles: {
Admin: 1,
User: 2,
NotRegistered: 0
}
};
angular.module('').const('Enums', Enums);
Karma isn't meant for integration testing. Your call to module('waterfall') is actually a reference to angular.mock.module which mocks all $https calls.
You need to use some form of end-to-end testing to test with real data. I suggest http://angular.github.io/protractor/#/.

Calling a method from an injected service in Jasmine

I'm attempted to unit test a service. I've injected the service however the method call getAllProducts() doesn't appear to run however the test still passes!
Plnkr
service.js
angular.module('vsApp')
.factory('productsDataService', function($http) {
var service = {
getAllProducts: getAllProducts
};
// get all products
function getAllProducts() {
return $http.get('/apiv1/getAllProducts/').then(function(data) {
return (data);
});
}
return service;
});
spec.js
// jasmine
describe('products data service', function () {
var $httpBackend, productsDataService;
beforeEach(module('vsApp'));
beforeEach(inject(function(_$httpBackend_, _productsDataService_) {
$httpBackend = _$httpBackend_;
productsDataService = _productsDataService_;
}));
it('should get all products', inject(function() {
console.info("get all");
// mock response for the http call in the service
$httpBackend.when('GET', '/apiv1/getAllProducts/')
.respond({name: 'item', price: '932'});
//this doesn't seem to run??
productsDataService.getAllProducts().then(function(response) {
expect(response.data.length).toBeGreaterThan(1);
});
}));
});
Ok, you have to make it sync. (all pending request will get resolved) using $http.flush();
Working demo as expected
productsDataService.getAllProducts().then(function(response) {
console.log(response);
expect(response.data.length).toBeGreaterThan(999);
});
$httpBackend.flush(); // <=============== here.

Resources