Testing angular $http inside promise chain with mocha - javascript

I have jsdom/mocha/chai set up for backend angular testing.
I have a service that essentially does this (intentionally no post data):
app.service('testService', ['config', '$http', function(config, $http) {
function getSpecificConfig(type) {
return config.getConfig()
.then(function(config) {
// config is coming back defined;
// $http timesout
return $http({method: 'post', url: 'http://localhost:2222/some/path', withCredentials: true});
})
.then(function(res) {
return res.data.config[type];
})
.catch(function(err) {
//handles err
});
};
return {
getConfig: getConfig
}
}]);
my test is:
/* jshint node: true */
/* jshint esversion: 6 */
let helpers = require(bootstrapTest),
inject = helpers.inject,
config,
specificConfig,
mockResponse,
$httpBackend,
$rootScope;
//config service
require('config.js');
//testService I'm testing
require('testService');
beforeEach(inject(function($injector, _$httpBackend_) {
config = $injector.get('config');
specificConfig = $injector.get('testService');
$rootScope = $injector.get('$rootScope');
$httpBackend = _$httpBackend_;
$httpBackend.when('POST', 'http://localhost:2222/some/path')
.response(function(data) {
//would like this to fire
console.log('something happened');
mockResponse = {data: 'some data'};
return mockResponse;
});
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectations();
$httpBackend.verifyNoOutstandingRequest();
});
describe ('this service', function() {
beforeEach(function() {
$httpBackend.expect('POST', 'http://localhost:2222/some/path');
$rootScope.$apply(function() {
return specificConfig('something');
});
});
it ('returns the specific config', function() {
expect(mockResponse).to.equal('some data');
})
});
Problem:
When the test is run, the config.getConfig() is resolving properly but the $http leads to a mocha timeout (2000ms) and the afterEach hook throws an Unsatisfied request.
My understanding of this may be completely incorrect so please feel free to educate me on the correct approach (here was my approach):
1) require all necessary dependencies.
2)inject them and set up a $httpBackend listener which fires the test response when the real http is fired.
3) $rootScope.$apply() any promises as the resolution of them is tied to the angular lifecycle.
4) the first before each sets the listener, the second before each fires the service which fires the $http allowing $httpBackend to fire and set the mockResponse.
5) test mock response.

If you need to return promises in your mocked HTTP requests you can use angular-mocks-async like so:
var app = ng.module( 'mockApp', [
'ngMockE2E',
'ngMockE2EAsync'
]);
app.run( [ '$httpBackend', '$q', function( $httpBackend, $q ) {
$httpBackend.whenAsync(
'GET',
new RegExp( 'http://api.example.com/user/.+$' )
).respond( function( method, url, data, config ) {
var re = /.*\/user\/(\w+)/;
var userId = parseInt(url.replace(re, '$1'), 10);
var response = $q.defer();
setTimeout( function() {
var data = {
userId: userId
};
response.resolve( [ 200, "mock response", data ] );
}, 1000 );
return response.promise;
});
}]);

Related

service promise not resolving with karma

I have a service factory that connects and interacts with an api for data. Here is the service:
angular.module('dbRequest', [])
.factory('Request', ['$http', 'localConfig', function($http, localConfig){
return {
getDataRevision: function(){
return $http({
url: localConfig.dbDataUrl,
method: "GET",
crossDomain: true,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
})
}
}
}]);
Taking cues from this answer, This is how I am testing the method:
describe('Service: Request', function () {
var $scope, srvMock, $q, lc, $httpBackend;
beforeEach(module('webApp'));
beforeEach(inject(function($rootScope, Request, _$q_, _$httpBackend_, localConfig){
$scope = $rootScope.$new();
srvMock = Request;
$q = _$q_;
$httpBackend = _$httpBackend_;
lc = localConfig;
$httpBackend.expect('GET', lc.dbDataUrl);
$httpBackend.when('GET', lc.dbDataUrl).respond({
success: ["d1","d2", "d3"]
});
}));
it('should return a promise', function(){
expect(srvMock.getDataRevision().then).toBeDefined();
});
it('should resolve with data', function(){
var data;
var deferred = $q.defer();
var promise = deferred.promise;
promise.then(function(res){
data = res.success;
});
srvMock.getDataRevision().then(function(res){
deferred.resolve(res);
});
$scope.$digest();
expect(data).toEqual(["d1","d2", "d3"]);
})
});
should return a promise passes, but the next should resolve with data fails with this error:
Expected undefined to equal [ 'd1', 'd2', 'd3' ].
However, the service method getDataRevision is getting called, but not getting resolved by the mock promise in the test. How do I make the correction?
Currently you are expecting mocked data to be there in data variable without flushing httpRequests, but that won't happen till you flush all the httpRequests. What $httpBackend.flush() does is, it returns mock data to that particular request that you have did using $httpBackend.when('GET', lc.dbDataUrl).respond.
Additionally you don't need to create extra promise which would be an overhead. Instead of having custom promise you could have utilize service function returned promise itself like below.
Code
it('should resolve with data', function(){
var data;
srvMock.getDataRevision().then(function(res){
data = res.success;
});
$scope.$digest();
$httpBackend.flush(); //making sure mocked response has been return
//after .then evaluation only below code will get called.
expect(data).toEqual(["d1","d2", "d3"]);
})

Angular - Testing http services with httpBackend throws unexpected exception

The module definition
var module = angular.module('test', []);
module.provider('client', function() {
this.$get = function($http) {
return {
foo: function() {
return $http.get('foo');
}
}
}
});
module.factory('service', ['client', function(client) {
return {
bar: function() {
return client.foo();
}
}
}]);
Basically, client is a wrapper for http calls, and service is a wrapper around the client basic features.
I'm unit testing both the provider and the service with karma+jasmine. The provider tests run as expected, but i have a problem with the service tests:
describe('service test', function(){
var service = null;
beforeEach(function(){
module('test')
inject(function(_service_, $httpBackend, $injector) {
service = _service_;
$httpBackend = $injector.get('$httpBackend');
});
});
it('should invoke client.foo via service.bar', function() {
$httpBackend.expect("GET", "foo");
service.bar();
expect($httpBackend.flush).not.toThrow();
});
});
I get Expected function not to throw, but it threw Error: No pending request to flush !.. When testing the provider with the same way, this test passes. Why?
When you are testing your service, you need to mock the client and inject that mock instead of the real client. Your mock can be in the same file if you only expect to use it for testing this service or in a separate file if you'll use it again elsewhere. Doing it this way does not require the use of $httpBackend (because you are not actually making an http call) but does require using a scope to resolve the promise.
The mock client:
angular.module('mocks.clientMock', [])
.factory('client', ['$q', function($q) {
var mock = {
foo: function() {
var defOjb = $q.defer();
defOjb.resolve({'your data':'1a'});
return defOjb.promise;
}
};
return mock;
}]);
Using the mock:
describe('service test', function(){
var service, mock, scope;
beforeEach(function(){
module('test', 'mocks.clientMock');
inject(function(_service_, $rootScope) {
service = _service_;
scope = $rootScope.$new();
});
});
it('should invoke client.foo via service.bar', function() {
spyOn(client, 'foo').and.callThrough();
service.bar();
scope.$digest();
expect(client.foo).toHaveBeenCalled();
});
});

Unittest case using Jasmine for http post method

I have tried to write a unit test case for post method in angular service. I got $http is undefined error. below is my code. any one tell me what i am missing.
i am adding module using separate file.
service code
sample.factory('AddProductTypeService', function () {
return {
exciteText: function (msg) {
return msg + '!!!'
},
saveProductType: function (productType) {
var result = $http({
url: "/Home/AddProductTypes",
method: "POST",
data: { productType: productType }
}).then(function (res) {
return res;
});
return result;
}
};
});
Jasmine
describe("AddProductTypeService UnitTests", function () {
var $rootScope, $scope, $factory, $httpBackend, basicService,createController, authRequestHandler;
beforeEach(function () {
module('sampleApp');
inject(function ($injector) {
basicService = $injector.get('AddProductTypeService');
// Set up the mock http service responses
$httpBackend = $injector.get('$httpBackend');
});
});
// check to see if it does what it's supposed to do.
it('should make text exciting', function () {
var result = basicService.exciteText('bar');
expect(result).toEqual('bar!!!');
});
it('should invoke service with right paramaeters', function () {
$httpBackend.expectPOST('Home/AddProductTypes', {
"productType": "testUser"
}).respond({});
basicService.saveProductType('productType');
$httpBackend.flush();
});
});
error :
ReferenceError: $http is not defined
Thanks in advance
You have to inject the $http service into your service
sample.factory('AddProductTypeService', ['$http' ,function ($http) {
/* ... */
}]);
https://docs.angularjs.org/guide/di

how to return rejected/failure promise to through jasmine spy to an angular controller in a unit test

I am using jasmine for testing my angular controllers.
I am catching errors and success in the .then(successCallback, errorCallback)
Although it is working fine on the bases of live functionality but am confused how to write a spy for returning an error as it is always caught in the successCallback()
Following is the controller :-
angular.module('myApp')
.controller('LoginCtrl', function ($scope, $location, loginService, SessionService) {
$scope.errorMessage = '';
$scope.login = function () {
var credentials = {
email: this.email,
password: this.password
};
SessionService.resetSession();
var request = loginService.login(credentials);
request.then(function(promise){ //successfull callback
if (promise['status'] === 200){
//console.log('login');
$location.path('/afterloginpath');
}
},
function(errors){ //fail call back
// console.log(errors);
$location.path('/login');
});
};
});
My test case :-
'use strict';
describe('Controller: LoginCtrl', function () {
// load the controller's module
beforeEach(module('myApp'));
var LoginCtrl, scope, location, login, loginReturn, session;
var credentials = {'email': 'email#email.com', 'password': 'admin123'};
// Initialize the controller and a mock scope
beforeEach(inject(function ($controller, $rootScope, $location, _loginService_, _SessionService_) {
scope = $rootScope.$new();
LoginCtrl = $controller('LoginCtrl', {
$scope: scope
});
location = $location;
login = _loginService_;
session = _SessionService_;
scope.errorMessage = '';
spyOn(login, "login").andCallFake(
function(){
return {
then: function(response){
response(loginReturn);
}
}
}
);
spyOn(session, "setName").andCallFake(function(){
return true;
});
}));
it('should go to login when login fail', function () {
loginReturn = function(){
return{
successfullyCallback: {throwError:true},
failCallback: {status: 400, 'data' : {'errors' : [{"type":"invalid_data","target":"email,password"}]}}
}
};
var wrong_creds = {email: 'wrong#email.com', password: 'wrong_password'};
scope.email = wrong_creds.email;
scope.password = wrong_creds.password;
scope.login();
expect(location.path()).toBe("/login");
expect(scope.errorMessage).toBe('username or password combination is invalid');
});
});
I find it easier to use actual promises for mocked services as it removes a lot of nested functions and is a lot easier to read.
Relevant snippets ($q needs to be injected in beforeEach):
deferred = $q.defer();
spyOn(login, 'login').andReturn(deferred.promise);
...
deferred.reject({ ... });
After resolving or rejecting the promise you need to call scope.$digest() so angular can process it.

AngularJS abort all pending $http requests on route change

Please go through the code first
app.js
var app = angular.module('Nimbus', ['ngRoute']);
route.js
app.config(function($routeProvider) {
$routeProvider
.when('/login', {
controller: 'LoginController',
templateUrl: 'templates/pages/login.html',
title: 'Login'
})
.when('/home', {
controller: 'HomeController',
templateUrl: 'templates/pages/home.html',
title: 'Dashboard'
})
.when('/stats', {
controller: 'StatsController',
templateUrl: 'templates/pages/stats.html',
title: 'Stats'
})
}).run( function($q, $rootScope, $location, $route, Auth) {
$rootScope.$on( "$routeChangeStart", function(event, next, current) {
console.log("Started");
/* this line not working */
var canceler = $q.defer();
canceler.resolve();
});
$rootScope.$on("$routeChangeSuccess", function(currentRoute, previousRoute){
$rootScope.title = ($route.current.title) ? $route.current.title : 'Welcome';
});
})
home-controller.js
app.controller('HomeController',
function HomeController($scope, API) {
API.all(function(response){
console.log(response);
})
}
)
stats-controller.js
app.controller('StatsController',
function StatsController($scope, API) {
API.all(function(response){
console.log(response);
})
}
)
api.js
app.factory('API', ['$q','$http', function($q, $http) {
return {
all: function(callback) {
var canceler = $q.defer();
var apiurl = 'some_url'
$http.get(apiurl,{timeout: canceler.promise}).success(callback);
}
}
}]);
When I move from home to stats , again API will send http request, I have many http calls like this, I pasted only few lines of code.
What I need is I need to cancel abort all pending http requests on routechangestart or success
Or any other way to implement the same ?
I put together some conceptual code for this. It might need tweaking to fit your needs. There's a pendingRequests service that has an API for adding, getting and cancelling requests, a httpService that wraps $http and makes sure all requests are tracked.
By leveraging the $http config object (docs) we can get a way to cancel a pending request.
I've made a plnkr, but you're going to need quick fingers to see requests getting cancelled since the test-site I found typically responds within half a second, but you will see in the devtools network tab that requests do get cancelled. In your case, you would obviously trigger the cancelAll() call on the appropriate events from $routeProvider.
The controller is just there to demonstrate the concept.
DEMO
angular.module('app', [])
// This service keeps track of pending requests
.service('pendingRequests', function() {
var pending = [];
this.get = function() {
return pending;
};
this.add = function(request) {
pending.push(request);
};
this.remove = function(request) {
pending = _.filter(pending, function(p) {
return p.url !== request;
});
};
this.cancelAll = function() {
angular.forEach(pending, function(p) {
p.canceller.resolve();
});
pending.length = 0;
};
})
// This service wraps $http to make sure pending requests are tracked
.service('httpService', ['$http', '$q', 'pendingRequests', function($http, $q, pendingRequests) {
this.get = function(url) {
var canceller = $q.defer();
pendingRequests.add({
url: url,
canceller: canceller
});
//Request gets cancelled if the timeout-promise is resolved
var requestPromise = $http.get(url, { timeout: canceller.promise });
//Once a request has failed or succeeded, remove it from the pending list
requestPromise.finally(function() {
pendingRequests.remove(url);
});
return requestPromise;
}
}])
// The controller just helps generate requests and keep a visual track of pending ones
.controller('AppCtrl', ['$scope', 'httpService', 'pendingRequests', function($scope, httpService, pendingRequests) {
$scope.requests = [];
$scope.$watch(function() {
return pendingRequests.get();
}, function(pending) {
$scope.requests = pending;
})
var counter = 1;
$scope.addRequests = function() {
for (var i = 0, l = 9; i < l; i++) {
httpService.get('https://public.opencpu.org/ocpu/library/?foo=' + counter++);
}
};
$scope.cancelAll = function() {
pendingRequests.cancelAll();
}
}]);
You can use $http.pendingRequests to do that.
First, when you make request, do this:
var cancel = $q.defer();
var request = {
method: method,
url: requestUrl,
data: data,
timeout: cancel.promise, // cancel promise, standard thing in $http request
cancel: cancel // this is where we do our magic
};
$http(request).then(.....);
Now, we cancel all our pending requests in $routeChangeStart
$rootScope.$on('$routeChangeStart', function (event, next, current) {
$http.pendingRequests.forEach(function(request) {
if (request.cancel) {
request.cancel.resolve();
}
});
});
This way you can also 'protect' certain request from being cancelled by simply not providing 'cancel' field in request.
I think this is the best solution to abort requests. It's using an interceptor and $routeChangeSuccess event.
http://blog.xebia.com/cancelling-http-requests-for-fun-and-profit/
Please notice that im new with Angular so this may not be optimal.
Another solution could be:
on the $http request adding the "timeout" argument, Docs I did it this way:
In a factory where I call all my Rest services, have this logic.
module.factory('myactory', ['$http', '$q', function ($http, $q) {
var canceler = $q.defer();
var urlBase = '/api/blabla';
var factory = {};
factory.CANCEL_REQUESTS = function () {
canceler.resolve();
this.ENABLE_REQUESTS();
};
factory.ENABLE_REQUESTS = function () {
canceler = $q.defer();
};
factory.myMethod = function () {
return $http.get(urlBase, {timeout: canceler.promise});
};
factory.myOtherMethod= function () {
return $http.post(urlBase, {a:a, b:b}, {timeout: canceler.promise});
};
return factory;
}]);
and on the angular app configuration I have:
return angular.module('app', ['ngRoute', 'ngSanitize', 'app.controllers', 'app.factories',
'app.filters', 'app.directives', 'ui.bootstrap', 'ngGeolocation', 'ui.select' ])
.run(['$location', '$rootScope', 'myFactory', function($location, $rootScope, myFactory) {
$rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
myFactory.CANCEL_REQUESTS();
$rootScope.title = current.$$route.title;
});
}]);
This way it catches all the "route" changes and stops all the request configured with that "timer" so you can select what is critical for you.
I hope it helps to someone.
Regards

Categories