Angular $q promise Unit testing using Mocha, Chai, Sinon - javascript

I'm trying to get my simple angualr $q promise unit test to work. But I've been having problems getting it to work.
Here's my angular file.
app.controller('theCtrl', ['$scope', '$q', function($scope, $q) {
$scope.addOne = function(num) {
var q = $q.defer();
if(angular.isNumber(num)) {
q.resolve(num+1);
} else {
q.reject('NaN');
}
return q.promise;
}
$scope.myVal = 0;
$scope.promise = $scope.addOne($scope.myVal);
// $scope.promise.then(function(v) {$scope.myVal = v }, function(err) {$scope.myVal = err});
}]);
I'm using Mocha, Chai and sinon for the unit testing.
Here's my test file.
describe("Contacts App", function() {
describe("the contact service", function(){
var $scope, theCtrl, $q;
beforeEach(module('Contacts'));
beforeEach(inject(function($injector) {
var $rootScope = $injector.get('$rootScope');
var $controller = $injector.get('$controller');
$scope = $rootScope.$new();
theCtrl = $controller('theCtrl', {$scope: $scope} );
$q = $injector.get('$q');
}));
it('should have a properly working promise', function() {
// Any answers?
});
});
});
Any suggestion would be really appreciated. Thanks, Cheers!

First proposition
You can use mocha's callback function to test asynchronous code along with the angular $timeout.flush()
it('should have a properly working promise', function(done) {
expect($scope.promise).to.be.defined;
$scope.promise.then(function() {
done();
});
$timeout.flush();
});
Second proposition (recommended by Mocha)
You can use https://github.com/domenic/chai-as-promised and return a promise. Your code should look as below
it('should increment the input if number given', function() {
return $scope.addOne(1).should.eventually.equal(2);
});

Related

Unit testing AngularJs with Jasmine: then - catch testing issue

I have a following piece of code (simplified):
angular
.module('myApp')
.controller('MyController', MyController);
function MyController(wordService) {
getWord();
function getWord() {
return wordService.getNextWord()
.then(doSomethingWithWord)
.catch(doSomethingFailure);
function doSomethingWithWord(response) {
// ... something
}
function doSomethingFailure() {
// ... failing
}
}
}
And I have to test it.
I'm struggling with this over a day now and I can't get it working :(
How to test this code?
For the future, I figured it out:
I have to use $q service and request Angular digest cycle.
describe('MyController', function () {
var $controller, myController, wordService, $q, deferredResponse, scope;
beforeEach(function() {
module('myApp');
inject(function(_$controller_, _wordService_, _$q_, $rootScope) {
$controller = _$controller_;
wordService = _wordService_;
scope = $rootScope.new();
$q = _$q_;
});
myController = $controller('MyController', {wordService:wordService});
deferredResponse = $q.defer(); //deferring asynchronous response
spyOn(wordService, 'getNextWord').and.returnValue(deferredResponse.promise);
});
describe('Testing WordService', function() {
it('Should get next word', function () {
deferredResponse.resolve({status: 200, data: {word: 123}});
scope.$apply();
expect(wordService.getNextWord).toHaveBeenCalled();
})
})
});

How to test an AngularJS factory method that returns a $timeout promise with a $http.get inside?

For specific purposes, I had to write a factory method that returns a $timeout promise that, inside of it, returns a $http.get promise.
I want to test if a call to my factory method will call the $http.get with the correct mocked URL (mocked-get-path).
Here is my factory code:
(function() {
'use strict';
angular.module('MyApp', []);
angular.module('MyApp')
.constant("ROUTES", {
get: "real-get-path"
});
angular.module('MyApp')
.factory('MyFactory', ['$http', '$timeout', 'ROUTES', MyFactory]);
function MyFactory($http, $timeout, ROUTES) {
return {
myGet: function(id) {
var random = Math.random() * 1000;
return $timeout(function () {
return $http.get(ROUTES.get, {
params: { id: id }
})
.then(function() {
return response.data;
});
}, random);
}
};
}
})();
And my test specification:
describe('simple factory test', function() {
var $http, $timeout, scope, MyFactory;
var ROUTES = {
get: 'mocked-get-path'
};
beforeEach(module('MyApp', function ($provide) {
$provide.constant('ROUTES', ROUTES);
}));
beforeEach(module('MyApp'));
beforeEach(inject(function(_$rootScope_, _$http_, _$timeout_, _MyFactory_) {
scope = _$rootScope_.$new();
$http = _$http_;
$timeout = _$timeout_;
MyFactory = _MyFactory_;
}));
it('should use ROUTES.get on method myGet', function(done) {
spyOn(Math, "random").and.returnValue(0.01);
MyFactory.myGet('elem1')
.then(function(res) {
expect($http.get).toHaveBeenCalledWith(ROUTES.get);
done();
});
$timeout.flush();
});
});
You can see that I tried to write a expect for the $http.get inside the then, but it didn't work.
I receive this error from Jasmine:
Error: Unexpected request: GET mocked-get-path?id=elem1
I made a Plunker: https://plnkr.co/edit/Ia6Q6GvKZOkNU2B8GrO1
What am I doing wrong?
When testing $http you are going to want to use $httpBackend.when
In your case:
it('should use ROUTES.get on method myGet', function(done) {
spyOn(Math, "random").and.returnValue(0.01);
spyOn($http, "get").and.callThrough();
MyFactory.myGet('elem1')
.then(function(res) {
expect($http.get).toHaveBeenCalledWith(ROUTES.get, {params: {id: 'elem1'}});
done();
});
$httpBackend
.when('GET', "mocked-get-path?id=elem1")
.respond(200, { foo: 'bar' });
$timeout.flush(100);
$timeout.verifyNoPendingTasks();
$httpBackend.flush();
});
This will cause $httpBackend to complete your request successfully allowing your .then with the expect to execute.
Plunkr showing solution
ngmock fundamentals
I hope this helps! Have a nice day!
One approach is to add expected calls to the httpBackend like this:
var $httpBackend;
beforeEach(function () {
inject(function ( _$httpBackend_ ) {
$httpBackend = _$httpBackend_;
}
....
it('....', function(){
//before the rest of the test
$httpBackend.expectGET('url').respond("OK");
});
https://docs.angularjs.org/api/ngMock/service/$httpBackend
So, your code will look like this:
describe('simple directive', function() {
var $http, $timeout, scope, MyFactory, $httpBackend;
var ROUTES = {
get: 'mocked-get-path'
};
beforeEach(module('MyApp', function ($provide) {
$provide.constant('ROUTES', ROUTES);
}));
beforeEach(module('MyApp'));
beforeEach(inject(function(_$rootScope_, _$http_, _$timeout_, _MyFactory_, _$httpBackend_) {
$httpBackend = _$httpBackend_;
scope = _$rootScope_.$new();
$http = _$http_;
$timeout = _$timeout_;
MyFactory = _MyFactory_;
}));
it('should use ROUTES.get on method myGet', function(done) {
spyOn(Math, "random").and.returnValue(0.01);
$httpBackend.expectGET("mocked-get-path?id=elem1").respond("OK");
MyFactory.myGet('elem1')
.then(function(res) {
expect($http.get).toHaveBeenCalledWith(ROUTES.get);
done();
});
$timeout.flush();
});
});
It will get you passed the unexpected GET error. But then there is another issue about not getting the call within the timeout period.
And, providing that now you are expecting a correct call at the $httpBackend, you can simplify your test case by removing the async nature of it like this:
(remove the paremeter done, and simplify the test code)
it('should use ROUTES.get on method myGet', function() {
spyOn(Math, "random").and.returnValue(0.01);
$httpBackend.expectGET("mocked-get-path?id=elem1").respond("OK");
MyFactory.myGet('elem1');
$timeout.flush();
});

How to test a factory with $resource which returns a promise in AngularJS?

In my Angular application, I have a controller defined as follows:
angular.module('myApp.controllers')
.controller('AppController', function($rootScope, $scope, CheckResource) {
$scope.check = function(data) {
var promise = CheckResource.query(data).$promise;
promise.then(function(result) {
$scope.value = result;
}, function() {
$scope.value = "default";
});
};
};
And my CheckResource is a Factory as follows:
angular.module('myApp.services', ['ngResource'])
.factory('CheckResource', function($resource) {
return $resource('/check', {}, {
query: {
method: 'POST'
}
});
});
I would like to write a unit test for my controller AppController and somehow mock my factory CheckResource, but I'm not sure how exactly I can achieve this using $q and deferred promise. Can anyone please explain me what's going on this scenario?
What are my options for testing this scenario? Should I mock my service? Should I use spyOn? Should I use $httpBackend? Or, should I do an E2E test?
Assuming you have a controller mocked in your test, you can do something like:
var deferred;
beforeEach(inject(function($q) {
deferred = $q.defer();
myController.CheckResource = {
query: function() {
return {$promise: deferred.promise};
}
};
}));
Then in your test itself:
it('tests CheckResource', function() {
deferred.resolve([]);
expect(something).toHaveHappened();
});
ngResource is defined in the separate module. Please check the documentation http://docs.angularjs.org/api/ngResource In other words you have to include and add ngResource dependency to MyApp.Factory module - angular.module("MyApp.Factory", ["ngResource"])
var factory;
beforeEach(function () {
module("MyApp.Factory");
inject(function (_factory_) {
factory = _factory_;
});
});

Why does injecting 'ng' in unit tests change promise handling behaviour?

The following service uses $q.when to wrap a third-party promise:
// service.js
angular.module('test', [])
.service('pouchdb', function($q, $window) {
var db = new $window.PouchDB('test');
this.info = function() {
return $q.when(db.info.apply(db, arguments));
};
});
Corresponding unit test:
describe('Failing Q when tests', function() {
beforeEach(module('test'));
var $rootScope, pouchdb;
beforeEach(inject(function(_$rootScope_, pouchdb) {
$rootScope = _$rootScope_;
pouchdb = pouchdb;
}));
it('should resolve a promise', function(done) {
// FIXME: never resolves
pouchdb.info()
.then(function(info) {
expect(info).toBeDefined();
})
.finally(done);
$rootScope.$apply();
});
});
pouchdb.info never resolves and Jasmine times out. However, if I manually inject ng, the spec works as expected:
describe('Working Q when tests', function() {
var pouchdb;
beforeEach(function() {
var $injector = angular.injector(['ng', 'test']);
var pouchDB = $injector.get('pouchdb');
pouchdb = pouchDB('db');
});
it('should resolve a promise', function(done) {
pouchdb.info()
.then(function(info) {
expect(info).toBeDefined();
})
.finally(done);
});
});
Could anyone explain why;
The first spec doesn't resolve
The second spec does (injecting ng)
It doesn't need $rootScope.$apply
Whether it's a good pattern to use
Are you using angular-mocks? https://docs.angularjs.org/api/ngMock
The only reason why I think that you'd need to inject 'ng' manually is if there is no ng-app initializing your app, at least according to https://docs.angularjs.org/api/ng/function/angular.module
If you use angular-mocks it takes care of that for you https://github.com/angular/angular.js/blob/master/src/ngMock/angular-mocks.js#L1785
Can't think of any other reason as to why this problem would occur.

Test a controller with success() and error ()

I'm trying to work out the best way to unit test success and error callbacks in controllers. I am able to mock out service methods, as long as the controller only uses the default $q functions such as 'then' (see the example below). I'm having an issue when the controller responds to a 'success' or 'error' promise. (Sorry if my terminology is not correct).
Here is an example controller \ service
var myControllers = angular.module('myControllers');
myControllers.controller('SimpleController', ['$scope', 'myService',
function ($scope, myService) {
var id = 1;
$scope.loadData = function () {
myService.get(id).then(function (response) {
$scope.data = response.data;
});
};
$scope.loadData2 = function () {
myService.get(id).success(function (response) {
$scope.data = response.data;
}).error(function(response) {
$scope.error = 'ERROR';
});
};
}]);
cocoApp.service('myService', [
'$http', function($http) {
function get(id) {
return $http.get('/api/' + id);
}
}
]);
I have the following test
'use strict';
describe('SimpleControllerTests', function () {
var scope;
var controller;
var getResponse = { data: 'this is a mocked response' };
beforeEach(angular.mock.module('myApp'));
beforeEach(angular.mock.inject(function($q, $controller, $rootScope, $routeParams){
scope = $rootScope;
var myServiceMock = {
get: function() {}
};
// setup a promise for the get
var getDeferred = $q.defer();
getDeferred.resolve(getResponse);
spyOn(myServiceMock, 'get').andReturn(getDeferred.promise);
controller = $controller('SimpleController', { $scope: scope, myService: myServiceMock });
}));
it('this tests works', function() {
scope.loadData();
expect(scope.data).toEqual(getResponse.data);
});
it('this doesnt work', function () {
scope.loadData2();
expect(scope.data).toEqual(getResponse.data);
});
});
The first test passes and the second fails with the error "TypeError: Object doesn't support property or method 'success'". I get that in this instance that getDeferred.promise
does not have a success function. Okay here is the question, what is a nice way to write this test so that I can test the 'success', 'error' & 'then' conditions of a mocked service ?
I'm starting to think that I should avoid the use of success() and error() in my controllers...
EDIT
So after thinking about this some more, and thanks to the detailed answer below, I've come to the conclusion that the handling the success and error callbacks in the controller is bad. As HackedByChinese mentions below success\error is syntactic sugar that is added by $http. So, in actual fact, by trying to handle success \ error I am letting $http concerns leak into my controller, which is exactly what I was trying to avoid by wrapping the $http calls in a service. The approach I'm going to take is to change the controller not to use success \ error:
myControllers.controller('SimpleController', ['$scope', 'myService',
function ($scope, myService) {
var id = 1;
$scope.loadData = function () {
myService.get(id).then(function (response) {
$scope.data = response.data;
}, function (response) {
$scope.error = 'ERROR';
});
};
}]);
This way I can test the error \ success conditions by calling resolve() and reject() on the deferred object:
'use strict';
describe('SimpleControllerTests', function () {
var scope;
var controller;
var getResponse = { data: 'this is a mocked response' };
var getDeferred;
var myServiceMock;
//mock Application to allow us to inject our own dependencies
beforeEach(angular.mock.module('myApp'));
//mock the controller for the same reason and include $rootScope and $controller
beforeEach(angular.mock.inject(function($q, $controller, $rootScope, $routeParams) {
scope = $rootScope;
myServiceMock = {
get: function() {}
};
// setup a promise for the get
getDeferred = $q.defer();
spyOn(myServiceMock, 'get').andReturn(getDeferred.promise);
controller = $controller('SimpleController', { $scope: scope, myService: myServiceMock });
}));
it('should set some data on the scope when successful', function () {
getDeferred.resolve(getResponse);
scope.loadData();
scope.$apply();
expect(myServiceMock.get).toHaveBeenCalled();
expect(scope.data).toEqual(getResponse.data);
});
it('should do something else when unsuccessful', function () {
getDeferred.reject(getResponse);
scope.loadData();
scope.$apply();
expect(myServiceMock.get).toHaveBeenCalled();
expect(scope.error).toEqual('ERROR');
});
});
As someone had mentioned in a deleted answer, success and error are syntactic sugar added by $http so they aren't there when you create your own promise. You have two options:
1 - Don't mock the service and use $httpBackend to setup expectations and flush
The idea is to let your myService act like it normally would without knowing it's being tested. $httpBackend will let you set up expectations and responses, and flush them so you can complete your tests synchronously. $http won't be any wiser and the promise it returns will look and function like a real one. This option is good if you have simple tests with few HTTP expectations.
'use strict';
describe('SimpleControllerTests', function () {
var scope;
var expectedResponse = { name: 'this is a mocked response' };
var $httpBackend, $controller;
beforeEach(module('myApp'));
beforeEach(inject(function(_$rootScope_, _$controller_, _$httpBackend_){
// the underscores are a convention ng understands, just helps us differentiate parameters from variables
$controller = _$controller_;
$httpBackend = _$httpBackend_;
scope = _$rootScope_;
}));
// makes sure all expected requests are made by the time the test ends
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
describe('should load data successfully', function() {
beforeEach(function() {
$httpBackend.expectGET('/api/1').response(expectedResponse);
$controller('SimpleController', { $scope: scope });
// causes the http requests which will be issued by myService to be completed synchronously, and thus will process the fake response we defined above with the expectGET
$httpBackend.flush();
});
it('using loadData()', function() {
scope.loadData();
expect(scope.data).toEqual(expectedResponse);
});
it('using loadData2()', function () {
scope.loadData2();
expect(scope.data).toEqual(expectedResponse);
});
});
describe('should fail to load data', function() {
beforeEach(function() {
$httpBackend.expectGET('/api/1').response(500); // return 500 - Server Error
$controller('SimpleController', { $scope: scope });
$httpBackend.flush();
});
it('using loadData()', function() {
scope.loadData();
expect(scope.error).toEqual('ERROR');
});
it('using loadData2()', function () {
scope.loadData2();
expect(scope.error).toEqual('ERROR');
});
});
});
2 - Return a fully-mocked promise
If the thing you're testing has complicated dependencies and all the set-up is a headache, you may still want to mock the services and the calls themselves as you have attempted. The difference is that you'll want to fully mock promise. The downside of this can be creating all the possible mock promises, however you could make that easier by creating your own function for creating these objects.
The reason this works is because we pretend that it resolves by invoking the handlers provided by success, error, or then immediately, causing it to complete synchronously.
'use strict';
describe('SimpleControllerTests', function () {
var scope;
var expectedResponse = { name: 'this is a mocked response' };
var $controller, _mockMyService, _mockPromise = null;
beforeEach(module('myApp'));
beforeEach(inject(function(_$rootScope_, _$controller_){
$controller = _$controller_;
scope = _$rootScope_;
_mockMyService = {
get: function() {
return _mockPromise;
}
};
}));
describe('should load data successfully', function() {
beforeEach(function() {
_mockPromise = {
then: function(successFn) {
successFn(expectedResponse);
},
success: function(fn) {
fn(expectedResponse);
}
};
$controller('SimpleController', { $scope: scope, myService: _mockMyService });
});
it('using loadData()', function() {
scope.loadData();
expect(scope.data).toEqual(expectedResponse);
});
it('using loadData2()', function () {
scope.loadData2();
expect(scope.data).toEqual(expectedResponse);
});
});
describe('should fail to load data', function() {
beforeEach(function() {
_mockPromise = {
then: function(successFn, errorFn) {
errorFn();
},
error: function(fn) {
fn();
}
};
$controller('SimpleController', { $scope: scope, myService: _mockMyService });
});
it('using loadData()', function() {
scope.loadData();
expect(scope.error).toEqual("ERROR");
});
it('using loadData2()', function () {
scope.loadData2();
expect(scope.error).toEqual("ERROR");
});
});
});
I rarely go for option 2, even in big applications.
For what it's worth, your loadData and loadData2 http handlers have an error. They reference response.data but the handlers will be called with the parsed response data directly, not the response object (so it should be data instead of response.data).
Don't mix concerns!
Using $httpBackend inside a controller is a bad Idea since you are mixing concerns inside your Test. Whether you retrieve data from an Endpoint or not is not a concern of the Controller, is a concern of the DataService you are calling.
You can see this more clearly if you change the Endpoint Url inside the service you will then have to modify both tests: the service Test and the Controller Test.
Also as previously mentioned, the use of success and error are syntactic sugar and we should stick to the use of then and catch. But in reality you may find yourself in the need of testing "legacy" code. So for that I'm using this function:
function generatePromiseMock(resolve, reject) {
var promise;
if(resolve) {
promise = q.when({data: resolve});
} else if (reject){
promise = q.reject({data: reject});
} else {
throw new Error('You need to provide an argument');
}
promise.success = function(fn){
return q.when(fn(resolve));
};
promise.error = function(fn) {
return q.when(fn(reject));
};
return promise;
}
By calling this function you will get a true promise that respond to then and catch methods when you need to and will also work for the success or error callbacks. Note that the success and error returns a promise itself so it will work with chained then methods.
(NOTE: On the 4th and 6th line the function returns resolve and reject values inside the data property of an object. This is to mock the Behavior of $http since it returns the data, http Status etc.)
Yes, do not use $httpbackend in your controller, because we don't need to make real requests, you just need to make sure that one unit is doing it's job exactly as expected, have a look on this simple controller tests, it's easy to understand
/**
* #description Tests for adminEmployeeCtrl controller
*/
(function () {
"use strict";
describe('Controller: adminEmployeeCtrl ', function () {
/* jshint -W109 */
var $q, $scope, $controller;
var empService;
var errorResponse = 'Not found';
var employeesResponse = [
{id:1,name:'mohammed' },
{id:2,name:'ramadan' }
];
beforeEach(module(
'loadRequiredModules'
));
beforeEach(inject(function (_$q_,
_$controller_,
_$rootScope_,
_empService_) {
$q = _$q_;
$controller = _$controller_;
$scope = _$rootScope_.$new();
empService = _empService_;
}));
function successSpies(){
spyOn(empService, 'findEmployee').and.callFake(function () {
var deferred = $q.defer();
deferred.resolve(employeesResponse);
return deferred.promise;
// shortcut can be one line
// return $q.resolve(employeesResponse);
});
}
function rejectedSpies(){
spyOn(empService, 'findEmployee').and.callFake(function () {
var deferred = $q.defer();
deferred.reject(errorResponse);
return deferred.promise;
// shortcut can be one line
// return $q.reject(errorResponse);
});
}
function initController(){
$controller('adminEmployeeCtrl', {
$scope: $scope,
empService: empService
});
}
describe('Success controller initialization', function(){
beforeEach(function(){
successSpies();
initController();
});
it('should findData by calling findEmployee',function(){
$scope.findData();
// calling $apply to resolve deferred promises we made in the spies
$scope.$apply();
expect($scope.loadingEmployee).toEqual(false);
expect($scope.allEmployees).toEqual(employeesResponse);
});
});
describe('handle controller initialization errors', function(){
beforeEach(function(){
rejectedSpies();
initController();
});
it('should handle error when calling findEmployee', function(){
$scope.findData();
$scope.$apply();
// your error expectations
});
});
});
}());

Categories

Resources