promise resolution with a mocked service within the tested service - javascript

My question: In Karma, I am mocking an injected service while testing the actual service. The mocked service gets some data, and sends back a promise. I can't figure out what I am doing wrong.
I know there are a number of issues with the actual "Login" mechanism, but I am using it to illustrate this Karma question. I do not plan to use it as production code. (However, any suggestions for a better illustration of the problem are welcome!)
First, I wrote this in a generic .js file, and said "node testcode.js"
testcode.js:
function Login(name,password,callback){
setTimeout(function(){
var response;
var promise = getByUserName();
promise.then(successCb);
function successCb(userObj){
if ( userObj != null && userObj.password === password ) {
response = { success : true };
} else {
response = { success: false, message: 'Username or password is incorrect' };
}
callback(response);
};
},200);
}
function getByUserName(){
return Promise.resolve(user);
}
var user = {
username : 'test',
id : 'testId',
password : 'test'
};
var test = undefined;
Login(user.username,user.password,testCb);
function testCb(response){
test = response;
console.log("Final: " + JSON.stringify(test));
}
This gives me my expected result:
Final: {"success":true}
Now, I try to repeat this in Karma...
TestService:
(function(){
"use strict";
angular.module('TestModule').service('TestService',TestService);
TestService.$inject = ['$http','$cookieStore','$rootScope','$timeout','RealService'];
})();
function TestService($http,$cookieStore,$rootScope,$timeout,RealService){
var service = {};
service.Login = Login;
/* more stuff */
return service;
function Login(username,password,callback){
$timeout(function () {
var response;
var promise = UserService.GetByUsername(username);
promise.then(successCb);
function successCb(user){
if (user !== null && user.password === password) {
response = { success: true };
} else {
response = { success: false, message: 'Username or password is incorrect' };
}
callback(response);
};
}, 1000);
}
}
My Karma-Jasmine Test:
describe('TestModule',function(){
beforeEach(module('TestModule'));
describe('TestService',function(){
var service,$rootScope,
user = {
username : 'test',
id : 'testId',
password : 'test'
};
function MockService() {
return {
GetByUsername : function() {
return Promise.resolve(user);
}
};
};
beforeEach(function(){
module(function($provide){
$provide.service('RealService',MockService);
});
inject(['$rootScope','TestService',
function($rs,ts){
$rootScope = $rs;
service = ts;
}]);
});
/**
* ERROR: To the best of my knowledge, this should not pass
*/
it('should Login',function(){
expect(service).toBeDefined();
var answer = {"success":true};
service.Login(user.username,user.password,testCb);
//$rootScope.$apply(); <-- DID NOTHING -->
//$rootScope.$digest(); <-- DID NOTHING -->
function testCb(response){
console.log("I'm never called");
expect(response.success).toBe(false);
expect(true).toBe(false);
};
});
});
});
Why is the promise not being resolved? I have tried messing with $rootScope.$digest() based on similar questions I have read on SO, but nothing seems to get testCb to be called.

Somebody let me know if I can give 'estus' the credit for this answer.
The magic is in the $timeout angular mock service:
https://docs.angularjs.org/api/ngMock/service/$timeout
and the $q service:
https://docs.angularjs.org/api/ng/service/$q
I updated two things. First, my MockUserService is using $q.defer instead of native promise. As 'estus' stated, $q promises are synchronous, which matters in a karma-jasmine test.
function MockUserService(){
return {
GetByUsername : function(unusedVariable){
//The infamous Deferred antipattern:
//var defer = $q.defer();
//defer.resolve(user);
//return defer.promise;
return $q.resolve(user);
}
};
};
The next update I made is with the $timeout service:
it('should Login',function(){
expect(service).toBeDefined();
service.Login(user.username,user.password,testCb);
$timeout.flush(); <-- NEW, forces the $timeout in TestService to execute -->
$timeout.verifyNoPendingTasks(); <-- NEW -->
function testCb(response) {
expect(response.success).toBe(true);
};
});
Finally, because I'm using $q and $timeout in my test, I had to update my inject method in my beforeEach:
inject([
'$q','$rootScope','$timeout','TestService',
function(_$q_,$rs,$to,ts) {
$q = _$q_;
$rootScope = $rs;
$timeout = $to;
service = ts;
}
]);

Related

AngularJs calling multiple service function : which is the best way to call multiple REST service on angularjs during the init step?

I have this infrastructure
[play] <-REST-> [karaf]
and this controller
$scope.getPrototypes = function (node) {
connectHttpService.getPrototypes(function (response) {
$scope.prototypes = response;
}, node);
}
$scope.getCommandsWithAck = function (node) {
connectHttpService.getCommands(function (response) {
$scope.commandsWithAck = response;
}, node, true);
}
$scope.getCommandsWithoutAck = function (node) {
connectHttpService.getCommands(function (response) {
$scope.commandsWithoutAck = response;
}, node, false);
}
where connectHttpService is the service
function getCommands(successHandler, failHandler, nodeId, ack) {
$http({
method: 'GET',
........MY ENDPOINT.........
}
}).success(function(response) {
successHandler(response);
}).error(function(response) {
console.log("connectHttpService got an error response: " + JSON.stringify(response));
})
}
the problem is my init method (called thorugh ng-init) is
$scope.initCommands = function () {
$scope.currentNode = $stateParams.node;
$scope.getPrototypes($scope.currentNode); //(1)
$scope.getCommandsWithAck($scope.currentNode); //(2)
$scope.getCommandsWithoutAck($scope.currentNode); //(3)
}
}
the $scope.getCommandsWithoutAck is called but doesn't return. On the server side (karaf) I see the call and the response. No error on the browser.
if I remove (1) or (2) it works
if I change the position of (3) and (2) it works, but only since (2) doesn't return nothing.
In other words: which is the best way to call multiple REST service on angularjs during the init step?
Try to use $q
Like this
$scope.currentNode = $stateParams.node;
$q.all([
$scope.getPrototypes($scope.currentNode), //(1)
$scope.getCommandsWithAck($scope.currentNode), //(2)
$scope.getCommandsWithoutAck($scope.currentNode) //(3)
]).then(function(result){
console.log(result[0]);//(1)
console.log(result[1]);//(2)
console.log(result[2]);//(3)
});
And you have to return promise from service
Like this
$scope.getCommandsWithAck = function (node) {
return connectHttpService.getCommands(function (response) {
$scope.commandsWithAck = response.data;
return $scope.commandsWithAck; //to complete promise
}, node, true);
}
N:B: you have to inject $q in your controller

Test Async $http real calls for angularjs

I want to do an e2e test of a angularjs service, without mocking the $http call. The call to fsSvc.getSubject results in multiple embedded async calls, eventually ending in calling the callback function where I have put the call to done.
Not sure this doesn't work - shouldn't $http make the call for real if its not mocked?:
it('should pass auth', function(done) {
inject(function (fsSvc) {
var cb = sinon.spy();
stubs.updateRecord = sinon.stub(dataSvc, 'updateRecord');
dataSvc.LO.famSrch.access_token_expires = false;
fsSvc.getSubject("abc", function(err, data) {
console.log("err:" + err);
done();
cb();
});
$timeout.flush();
expect(dataSvc.LO.famSrch.discovery).to.not.be.undefined;
expect(dataSvc.LO.famSrch.discovery.links).to.not.be.undefined;
expect(dataSvc.LO.famSrch.access_token).to.not.be.undefined;
expect(dataSvc.LO.famSrch.username).to.equal("test");
expect(dataSvc.LO.famSrch.password).to.equal(btoa("test"));
expect(dataSvc.LO.famSrch.access_token_expires).to.be.greaterThan(3600000);
expect(stubs.updateRecord.callCount).to.equal(1);
expect(stubs.updateRecord.args[0][1]).to.equal("localOption");
expect(cb.callCount).to.equal(1);
})
});
I don't see any issue with including e2e. Why is that an issue for you?
If you do decide to give ngMockE2E a try than keep in mind that it does not handle async / promise responses.
For example, if your mock response is a promise it won't work. Instances like going to the DB or using something like WebSQL / IndexedDB ( or other in memory DB ) won't work.
I have developed an angular plugin called angular-mocks-async to work it out. Here is an example of a mock:
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;
});
}]);

Error: No pending request to flush - when unit testing AngularJs service

I'm newbie to AngularJs, and I'm in the process of writing my first unit test; to test the service I wrote a test that simply returns a single Json object. However, everytime I run the test I get the error stated in the title. I don't know what exactly is causing this! I tried reading on $apply and $digest and not sure if that's needed in my case, and if yes how; a simple plunker demo would be appreciated.
here is my code
service
var allBookss = [];
var filteredBooks = [];
/*Here we define our book model and REST api.*/
var Report = $resource('api/books/:id', {
id: '#id'
}, {
query: {
method: 'GET',
isArray: false
}
});
/*Retrive the requested book from the internal book list.*/
var getBook = function(bookId) {
var deferred = $q.defer();
if (bookId === undefined) {
deferred.reject('Error');
} else {
var books= $filter('filter')(allBooks, function(book) {
return (book.id == bookId);
});
if (books.length > 0) {
deferred.resolve(books[0]);//returns a single book object
} else {
deferred.reject('Error');
};
};
return deferred.promise;
};
test
describe('unit:bookService', function(){
beforeEach(module('myApp'));
var service, $httpBackend;
beforeEach(inject(function (_bookService_, _$httpBackend_) {
service = _bookService_;
$httpBackend = _$httpBackend_;
$httpBackend.when('GET', "/api/books/1").respond(200, {
"book": {
"id": "1",
"author": "James Spencer",
"edition": "2",
.....
}
});
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should return metadata for single report', function() {
service.getBook('1').then(function(response) {
expect(response.length).toEqual(1);
});
$httpBackend.flush();// error is in this line
});
});
error
Error: No pending request to flush !
at c:/myapp/bower_components/angular-mocks/angular-mocks.js:1439
at c:/myapptest/tests/bookTest.js:34
libs version
AngularJS v1.2.21
AngularJS-mock v1.2.21
I don't see where you're actually issuing a Report.query(). The getBook function just returns an unresolved promise that will never be resolved because nothing in the function is async.
You need to call Report.query via the book function with the promise resolved in the .then() (in the book function). After that, flush the http backend in the service.getBook().then() and do the expect.

Function statement requires a Name in Services in AngularJS

I am building a demo app. I want this AngularJS app to have factory as well.
I keep getting error: "SyntaxError: function statement requires a name"
Below is my code:
var bookApp = angular.module('bookAppModule',[]);
bookApp.controller('boookbAppCtrl', ['$scope','$http',Book ,
function($scope,$http,Book) {
$scope.way=["Normal","$http","RestFul"];
$scope.books =
[
{"title":"abc","author":"zxc"},
{"title":"def","author":"cvb"},
{"title":"ghi","author":"nml"},
{"title":"jkl","author":"kjh"},
{"title":"mno","author":"fds"}
];
var names=["Anuj","Donvir"];
$scope.newbooks = Book.getBooks;
}]);
bookApp.factory('Book',
function(){
getBooks : function(){
return
[
{"title":"newbook1","author":"zxc"},
{"title":"newbook2","author":"cvb"},
{"title":"newbook3","author":"nml"},
{"title":"newbook4","author":"kjh"},
{"title":"newbook5","author":"fds"}
];
}
});
In your factory, you forgot to write the overall return function that returns all the 'methods' in the service.
bookApp.factory('Book',
function(){
return { // you did not have this return, only its body
getBooks : function(){
return
[
{"title":"newbook1","author":"zxc"},
{"title":"newbook2","author":"cvb"},
{"title":"newbook3","author":"nml"},
{"title":"newbook4","author":"kjh"},
{"title":"newbook5","author":"fds"}
];
}
}
});
Addition
Also, to add on the above reason which caused the error, personally i battled for many days with a similar error in angular services. It was caused by the overall return function not being in the same line with the { that contains the body of the return. See below.
// will cause the error
bookApp.factory('Book',
function(){
return
{ // { below not on same line with return
getBooks : function(){
// ...
}
}
});
// will not cause the error
bookApp.factory('Book',
function(){
return { // { on same line with return
getBooks : function(){
// ...
}
}
});
I do not know the exact reason for this behaviour but what i know is that when you use them on same line, it will work and you will not have to stall your project like i did many times.
If you need to forget the position of the braces, you can define this angular factory using Revealing Module Pattern as like this...
bookApp.factory('Book',
function(){
var factoryServices=
{
getBooks: getBooks
};
return factoryServices;
function getBooks()
{
// ...
}
}
});
For more reading: https://github.com/johnpapa/angular-styleguide#style-y052

AngularJs - doesn't skip request when waiting for new token

I have implemented authentication system and after upgrading from angular 1.0.8 to 1.2.x,
system doesn't work as it used to. When user logs in it gets a token. When token is expired,
a refresh function for new token is called. New token is successfully created on a server and it is
stored to database. But client doesn't get this new token, so it requests a new token again,
and again and again until it logs out. Server side (MVC Web Api) is working fine, so problem must
be on client side. The problem must be on a retry queue. Below I pasted relevant code and
a console trace for both versions of applications (1.0.8 and 1.2.x).
I am struggling with this for days now and I can't figure it out.
In the link below, there are 5 relevant code blocks:
interceptor.js (for intercepting requests, both versions)
retryQueue.js (manages queue of retry requests)
security.js (manages handler for retry queue item and gets a new token from api)
httpHeaders.js (sets headers)
tokenHandler.js (handles tokens in a cookies)
Code: http://pastebin.com/Jy2mzLgj
Console traces for app in angular 1.0.8: http://pastebin.com/aL0VkwdN
and angular 1.2.x: http://pastebin.com/WFEuC6WB
interceptor.js (angular 1.2.x version)
angular.module('security.interceptor', ['security.retryQueue'])
.factory('securityInterceptor', ['$injector', 'securityRetryQueue', '$q',
function ($injector, queue, $q) {
return {
response: function(originalResponse) {
return originalResponse;
},
responseError: function (originalResponse) {
var exception;
if (originalResponse.headers){
exception = originalResponse.headers('x-eva-api-exception');
}
if (originalResponse.status === 401 &&
(exception === 'token_not_found' ||
exception === 'token_expired')){
queue.pushRetryFn(exception, function retryRequest() {
return $injector.get('$http')(originalResponse.config);
});
}
return $q.reject(originalResponse);
}
};
}])
.config(['$httpProvider', function($httpProvider) {
$httpProvider.interceptors.push('securityInterceptor');
}]);
retryQueue.js
angular.module('security.retryQueue', [])
.factory('securityRetryQueue', ['$q', '$log', function($q, $log) {
var retryQueue = [];
var service = {
onItemAddedCallbacks: [],
hasMore: function(){
return retryQueue.length > 0;
},
push: function(retryItem){
retryQueue.push(retryItem);
angular.forEach(service.onItemAddedCallbacks, function(cb) {
try {
cb(retryItem);
}
catch(e){
$log.error('callback threw an error' + e);
}
});
},
pushRetryFn: function(reason, retryFn){
if ( arguments.length === 1) {
retryFn = reason;
reason = undefined;
}
var deferred = $q.defer();
var retryItem = {
reason: reason,
retry: function() {
$q.when(retryFn()).then(function(value) {
deferred.resolve(value);
}, function(value){
deferred.reject(value);
});
},
cancel: function() {
deferred.reject();
}
};
service.push(retryItem);
return deferred.promise;
},
retryAll: function() {
while(service.hasMore()) {
retryQueue.shift().retry();
}
}
};
return service;
}]);
security.js
angular.module('security.service', [
'session.service',
'security.signin',
'security.retryQueue',
'security.tokens',
'ngCookies'
])
.factory('security', ['$location', 'securityRetryQueue', '$q', /* etc. */ function(){
var skipRequests = false;
queue.onItemAddedCallbacks.push(function(retryItem) {
if (queue.hasMore()) {
if(skipRequests) {return;}
skipRequests = true;
if(retryItem.reason === 'token_expired') {
service.refreshToken().then(function(result) {
if(result) { queue.retryAll(); }
else {service.signout(); }
skipRequests = false;
});
} else {
skipRequests = false;
service.signout();
}
}
});
var service = {
showSignin: function() {
queue.cancelAll();
redirect('/signin');
},
signout: function() {
if(service.isAuthenticated()){
service.currentUser = null;
TokenHandler.clear();
$cookieStore.remove('current-user');
service.showSignin();
}
},
refreshToken: function() {
var d = $q.defer();
var token = TokenHandler.getRefreshToken();
if(!token) { d.resolve(false); }
var session = new Session({ refreshToken: token });
session.tokenRefresh(function(result){
if(result) {
d.resolve(true);
TokenHandler.set(result);
} else {
d.resolve(false);
}
});
return d.promise;
}
};
return service;
}]);
session.service.js
angular.module('session.service', ['ngResource'])
.factory('Session', ['$resource', '$rootScope', function($resource, $rootScope) {
var Session = $resource('../api/tokens', {}, {
create: {method: 'POST'}
});
Session.prototype.passwordSignIn = function(ob) {
return Session.create(angular.extend({
grantType: 'password',
clientId: $rootScope.clientId
}, this), ob);
};
Session.prototype.tokenRefresh = function(ob) {
return Session.create(angular.extend({
grantType: 'refresh_token',
clientId: $rootScope.clientId
}, this), ob);
};
return Session;
}]);
Thanks to #Zerot for suggestions and code samples, I had to change part of the interceptor like this:
if (originalResponse.status === 401 &&
(exception === 'token_not_found' || exception === 'token_expired')){
var defer = $q.defer();
queue.pushRetryFn(exception, function retryRequest() {
var activeToken = $cookieStore.get('authorization-token').accessToken;
var config = originalResponse.config;
config.headers.Authorization = 'Bearer ' + activeToken;
return $injector.get('$http')(config)
.then(function(res) {
defer.resolve(res);
}, function(err)
{
defer.reject(err);
});
});
return defer.promise;
}
Many thanks,
Jani
Have you tried to fix the error you have in the 1.2 log?
Error: [ngRepeat:dupes] Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: project in client.projects, Duplicate key: string:e
That error is at the exact point where you would need to see the $httpHeaders set line. It looks like your session.tokenrefresh is not working(and that code is also missing from the pastebin so I can't check.)
Interceptors should always return a promise.
So in responseError, you should better return $q.reject(originalResponse); instead of just return originalResponse.
Hope this helps
I think that your interceptor returns wrong result in errorResponse method.
I faced some "undebuggable" issue for the same reason.
With this code you could fall in some infinite loop flow...
Hope this helps.

Categories

Resources