I need to have created the following unit test that relies on a promise in a service being resolved, but the finally() callback is never called. The promise works just fine in the real application. I have read in various places that I need to kick off a digest cycle but that doesn't work. I'm using ui-router and it just starts an $stateChangeStart request and tries to retrieve the template of the first state. (Hence the $httpBackend mock for that).
var $rootScope;
var scope;
var $httpBackend;
var FormulaValidator;
var mockFunctionApiBaseUrl = 'http://localhost:5555';
beforeEach(function() {
module('ps', function($provide) {
$provide.constant('functionApiBaseUrl', mockFunctionApiBaseUrl);
$provide.value('identity', {
getUsernameFromLocalStorage: function() {
console.log('getting mock username from local storage');
return 'BLAH';
},
verifyToken: function(token) {
return true;
}
});
});
beforeEach(function(done) {
inject(function(_$httpBackend_, _$rootScope_, _FormulaValidator_) {
$httpBackend = _$httpBackend_;
$rootScope = _$rootScope_;
scope = $rootScope.$new();
FormulaValidator = _FormulaValidator_;
$httpBackend.expect('GET', mockFunctionApiBaseUrl + '/api/list/functions').respond(200, '{"MA": {}}');
$httpBackend.expect('GET', '/0.1.1/json/assets.json').respond(200, '["AAPL US EQUITY"]');
$httpBackend.expect('GET', '/null/templates/dashboard.html').respond(200, '<html></html>');
done();
})
});
afterEach(function() {
$httpBackend.flush();
});
it('Basic Validation 1', function (done) {
FormulaValidator.promise.finally(function () {
console.log('FormulaValidator.spec.promise finally');
var p = FormulaValidator.validateFormula('MA(AAPL US EQUITY, 30)');
console.log('getFunctions: ' + FormulaValidator.getFunctions().length);
expect(p).toBe(true);
done();
});
scope.$apply();
//$rootScope.$digest();
});
An $http promise will only be resolved when you flush the $httpBackend.
Flushing it in afterEach() is too late: the point of flushing $httpBackend is to tell it: OK, now you're supposed to have received requests, send back the response so that the promise is resolved with what I've told you to send back when calling $httpBackend.expect().
Read more about it is the doc.
Related
I am working on an angular js app with karma/Jasmine testing framework, I need to test a factory that returns a http promise but it always return undefined
here is my factory
angular.module('GithubUsers').factory('Users',['$http','$q',function($http,$q){
return{
getAllUsers:function(){
var defered= $q.defer();
$http({
url:'https://api.github.com/users',
method:'GET'
}).then(function(users){
defered.resolve(users.data);
},function(err){
defered.reject(err);
})
return defered.promise;
}
}
}])
here is my tests
Update thanks to your answers I modified my code to the following but no I got this error
Possibly unhandled rejection: {"status":0,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"jsonpCallbackParam":"callback","url":"https://api.github.com/users?since=1","headers":{"Accept":"application/json, text/plain, /"},"cached":false},"statusText":""} thrown
describe('Test Users Factory',function(){
var $controller,
Users,
$rootScope,
$httpBackend,
$q;
beforeEach(module('GithubUsers'));
beforeEach(inject(function(_$controller_,_Users_,_$rootScope_,_$httpBackend_,_$q_){
$controller = _$controller_;
Users = _Users_;
$rootScope= _$rootScope_;
$httpBackend=_$httpBackend_;
}))
it('should get users',function(){
var result;
$httpBackend.whenGET('https://api.github.com/users?since=1').respond(function(){
return {data:[{id:2}],status:200};
})
Users.getAllUsers().then(function(res){
result = res;
});
$httpBackend.flush();
$rootScope.$digest()
expect(result).toBeTruthy();
})
})
Thanks in advance!
I think you need to pass a function that returns a array with 3 items in it, to whenGET().respond().
Maybe, you can try something like this:
beforeEach(angular.mock.inject(function (User, $httpBackend, $http) {
...
this.withOKUsers = function() {
var i1 = new User();
i1.id = 10;
return [200, JSON.stringify([ i1]), {}];
} ...
}));
...
it('should get users',function(){
$httpBackend
.whenGET('https://api.github.com/users')
.respond(this.withOKUsers);
Users.getAllUsers().then(function(res){
result = res;
});
$httpBackend.flush();
expect(result).not.toBeNull();
...
(I prefer to arrange spec outside of it() clause for better readability)
You're missing a $httpBackend.flush(); call after your test method call. It will invoke a success/error or then part and resolve a $q's promise properly. For more tests I would move a $httpBackend.whenGET to each test case separately so I can later verify it per use case but it's just my personal opinion.
I find it a little suspicious that you mix a $controller and a factory in one test. I would suggest to split them, and in controller test just check the calls to service methods and in a facotry test itself do a $httpBackend stuff.
Below I paste your test with my corrections. It works now for me:
describe('Test Users Factory', function () {
var Users,
$rootScope,
$httpBackend,
$q;
beforeEach(module('app.utils'));
beforeEach(inject(function (_Users_, _$rootScope_, _$httpBackend_, _$q_) {
Users = _Users_;
$rootScope = _$rootScope_;
$httpBackend = _$httpBackend_;
}));
afterEach(function () {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should get users', function () {
var result;
$httpBackend.when('GET', "https://api.github.com/users").respond({ data: [{ id: 2 }], status: 200 });
Users.getAllUsers().then(function (res) {
result = res;
expect(result).toBeTruthy();
});
$httpBackend.flush();
$rootScope.$digest();
});
Important notices:
1)afterEach - check if no pending requests remain after your call
2) your url differ with a parameter ?since=1. But you do not give it as a parameter in your code so i do not understand why you added this parameter.
Maybe consider string concatenation with url and parameter ?
I have some code that runs a timer when user is active and broadcasts and event to keep the session alive.
In the controller I have some code to listen for that event and refresh the session.
I want to test this listener
$scope.$on('Keepalive', function () {
//every 45 minutes make a call to refresh the session.
var promise = authService.keepAliveSession();
promise.then(function(userPreferenceData) {
dataTransfer.setUserPref(userPreferenceData);
}, function(error) {
console.log("promise error!!"+error);
});
});
my keepalive service returns a promise which will get resolved after the httpbackend returns.
factory.keepAliveSession = function () {
var deferred = $q.defer();
req=...some stuff...
$http(req)
.success(
function (data, status) {
if ( data.user ) {
// Received data about the logged in user
deferred.resolve(factory.userPreferenceData);
} else {
// User is not authenticated; redirect to login
$window.location = data.redirect;
}
}
).error(function (error) {
// Error in oAuth refresh service
deferred.reject("Error in session keepalive"+ error);
});
return deferred.promise;
}
here is the test
it('Test for keepalive', function() {
console.log('starting for keepalive...');
httpBackend.when('POST', "http://example.com/refreshSession").respond(getMocks().response.oAuthResponseExternal);
spyOn(authServiceMock, "keepAliveSession").and.callThrough();
spyOn(dataTransferMock, "setUserPref").and.callThrough();
rootScope.$broadcast('Keepalive');
expect(authServiceMock.keepAliveSession).toHaveBeenCalled();
rootScope.$digest;
expect(dataTransferMock.setUserPref).toHaveBeenCalled();
});
The first assertion is successful, but the second one (after the digest) fails.
How can I force the promise to be resolved or rejected?
Do I need to somehow mock the $q?
It's hard to say for sure without seeing the mock implementations, how you wire up the module and how you create the controller.
My guess is that the promise that is returned from authServiceMock.keepAliveSession never gets resolved, which would lead to that the success function that dataTransfer.setUserPref lives in never gets executed.
If you for example have a mock that looks like this:
var deferred;
var authServiceMock = {
keepAliveSession: function () {
deferred = q.defer();
return deferred.promise;
}
};
You need to manually in your test either resolve or reject the promise before you trigger the digest cycle, depending on which case you are testing:
expect(authServiceMock.keepAliveSession).toHaveBeenCalled();
deferred.resolve('something');
$rootScope.$digest();
expect(dataTransferMock.setUserPref).toHaveBeenCalled();
Note that you need to execute the $digest function, in your example you just have rootScope.$digest;.
On another note, it seems to me you are mixing testing concerns a bit.
From my point of view, this is what your controller should test:
When the Keepalive event is fired - authService.keepAliveSession should be called
If the promise from authService.keepAliveSession is resolved - dataTransfer.setUserPref should be called and pass the correct data
If the promise from authService.keepAliveSession is rejected - an error message should be logged
The implementation details of the services shouldn't matter (other than that authService.keepAliveSession returns a promise) and you shouldn't need to involve httpBackend in this case.
Setting up fake return data with httpBackend should be used when you test the actual service that uses the $http service.
Below is an alternative way to test this, using spyOn and callFake instead of using mock implementations.
describe('myApp', function() {
var $rootScope;
var $controller;
var authService;
var dataTransfer;
var $log;
var myController;
beforeEach(function() {
module('myApp');
inject(function(_$rootScope_, _$controller_, _authService_, _dataTransfer_, _$q_, _$log_) {
$rootScope = _$rootScope_;
$controller = _$controller_;
authService = _authService_;
dataTransfer = _dataTransfer_;
$q = _$q_;
$log = _$log_;
});
myController = $controller('MyController', {
$scope: $rootScope.$new()
});
});
it('On event "Keepalive" - should call "authService.keepAliveSession"', function() {
spyOn(authService, 'keepAliveSession').and.callFake(function() {
var deferred = $q.defer();
return deferred.promise;
});
$rootScope.$broadcast('Keepalive');
expect(authService.keepAliveSession).toHaveBeenCalled();
});
it('Promise from "authService.keepAliveSession" is resolved - should call "dataTransfer.setUserPref"', function() {
var data = {};
spyOn(authService, 'keepAliveSession').and.callFake(function() {
var deferred = $q.defer();
deferred.resolve(data);
return deferred.promise;
});
spyOn(dataTransfer, 'setUserPref');
$rootScope.$broadcast('Keepalive');
$rootScope.$digest();
expect(dataTransfer.setUserPref).toHaveBeenCalledWith(data);
});
it('Promise from "authService.keepAliveSession" is rejected - should not call "dataTransfer.setUserPref"', function() {
var data = {};
spyOn(authService, 'keepAliveSession').and.callFake(function() {
var deferred = $q.defer();
deferred.reject(data);
return deferred.promise;
});
spyOn(dataTransfer, 'setUserPref');
$rootScope.$broadcast('Keepalive');
$rootScope.$digest();
expect(dataTransfer.setUserPref).not.toHaveBeenCalled();
});
it('Promise from "authService.keepAliveSession" is rejected - should log message', function() {
var error = {};
spyOn(authService, 'keepAliveSession').and.callFake(function() {
var deferred = $q.defer();
deferred.reject(error);
return deferred.promise;
});
spyOn($log, 'log');
$rootScope.$broadcast('Keepalive');
$rootScope.$digest();
expect($log.log).toHaveBeenCalledWith(error);
});
});
Demo: http://plnkr.co/edit/L1uks0skH2N5bAXGoe90?p=preview
Im new to testing and im trying to test my angular code in Jasmine. Im stuck on the problem of testing the answer from an resolved promise. Right now the test gets timed out. I would like to have the test waiting for the respons instead of just put in a mockup respons. How do i do that? Is it a bad way of making unit-tests?
angular.module("Module1", ['ng']).factory("Factory1", function($q){
function fn1(){
var deferred = $q.defer();
setTimeout(function(){ deferred.resolve(11); }, 100); // this is representing an async action
return deferred.promise;
}
return { func1 : fn1 };
});
describe('test promise from factory', function() {
var factory1, $rootScope, $q;
beforeEach(module('Module1'));
beforeEach(inject(function(Factory1, _$rootScope_, _$q_) {
factory1=Factory1;
$rootScope = _$rootScope_;
$q = _$q_;
}));
it('should be get the value from the resolved promise', function(done) {
factory1.func1().then(function(res){
expect(res).toBe(11);
done(); // test is over
});
$rootScope.$digest();
});
});
The setTimeout() block represents an async function call, and i dont want to replace it with something like $timeout.
I don't know why you wouldn't want to use the $timeout service.
But if you really want to use the setTimeout, a $rootScope.$digest() is required inside the callback.
function fn1() {
var deferred = $q.defer();
// this is representing an async action
setTimeout(function() {
deferred.resolve(11);
$rootScope.$digest(); // this is required ($timeout do this automatically).
}, 100);
return deferred.promise;
}
I use $q.when to wrap around other lib promises.
It works like a charm but when i try to run it inside Karma the promise failes to resolve (done() is never executed) even if I ran $digest and even after timeout.
Here is sample code:
describe('PouchDB', function () {
var $q, $rootScope;
beforeEach(inject(function (_$rootScope_, _$q_) {
$rootScope = _$rootScope_;
$q = _$q_;
}));
it("should run", function (done) {
function getPromise() {
var deferred = Q.defer();
deferred.resolve(1);
return deferred.promise;
}
$q.when(getPromise())
.then(function () {
done(); // this never runs
});
$rootScope.$digest();
});
Why? What is the cause of this? I really cannot get it.
Update (workaround)
I do not understand why $q.when is not resolved in Karma - it has something with nextTick function but I cannot debug the problem.
Instead I ditched $q.when and wrote simple function that converts PouchDB (or any other like Q) to $q:
.factory('$utils', function ($q, $rootScope) {
return {
to$q: function (promise) {
var deferred = $q.defer();
promise.then(function (result) {
deferred.resolve(result);
$rootScope.$digest();
});
promise.catch(function (error) {
deferred.reject(error);
$rootScope.$digest();
});
return deferred.promise;
}
}
})
From How to resolve $q.all promises in Jasmine unit tests? it seems the trick is:
$rootScope.$apply();
I just had the same problem and this works for me; the promises are resolved on making this call.
I've adjusted variable and injected dependency names on this to keep things clear as test writing continues. If done() is a function inside your (controller? directive? service/factory?) then it should be called when the test runs without trying to pass it in as a dependency. Ideally done() should be spied upon, but without knowing where it comes from it is impossible to show you how to set up the spy function.
The only other detail missing is that you have no expect() in this test suite. Without it I have no way to know what you are expecting to be asserted.
describe('PouchDB', function () {
var scope, db, q, rootScope;
beforeEach(
inject(
function(_$rootScope_, _$q_){
rootScope = _$rootScope_;
scope = rootScope.$new();
q = _$q_;
}
)
);
it("should run", function(){
//spy should be constructed here
function getPromise() {
var deferred = q.defer();
deferred.resolve(1);
return deferred.promise;
}
q.when(getPromise)
.then(function () {
done();
});
scope.$digest();
//assertion should be here
});
});
I'm trying to write a karma/jasmine test and I would like some explanations about how mocks are working on a service which is returning a promise. I explain my situation :
I have a controller in which I do the following call :
mapService.getMapByUuid(mapUUID, isEditor).then(function(datas){
fillMapDatas(datas);
});
function fillMapDatas(datas){
if($scope.elements === undefined){
$scope.elements = [];
}
//Here while debugging my unit test, 'datas' contain the promise javascript object instead //of my real reponse.
debugger;
var allOfThem = _.union($scope.elements, datas.elements);
...
Here is how my service is :
(function () {
'use strict';
var serviceId = 'mapService';
angular.module('onmap.map-module.services').factory(serviceId, [
'$resource',
'appContext',
'restHello',
'restMap',
serviceFunc]);
function serviceFunc($resource, appContext, restHello, restMap) {
var Maps = $resource(appContext+restMap, {uuid: '#uuid', editor: '#editor'});
return{
getMapByUuid: function (uuid, modeEditor) {
var maps = Maps.get({'uuid' : uuid, 'editor': modeEditor});
return maps.$promise;
}
};
}
})();
And finally, here is my unit test :
describe('Map controller', function() {
var $scope, $rootScope, $httpBackend, $timeout, createController, MapService, $resource;
beforeEach(module('onmapApp'));
beforeEach(inject(function($injector) {
$httpBackend = $injector.get('$httpBackend');
$rootScope = $injector.get('$rootScope');
$scope = $rootScope.$new();
var $controller = $injector.get('$controller');
createController = function() {
return $controller('maps.ctrl', {
'$scope': $scope
});
};
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
var response = {"elements":[1,2,3]};
it('should allow user to get a map', function() {
var controller = createController();
$httpBackend.expect('GET', '/onmap/rest/map/MY-UUID?editor=true')
.respond({
"success": response
});
// hope to call /onmap/rest/map/MY-UUID?editor=true url and hope to have response as the fillMapDatas parameter
$scope.getMapByUUID('MY-UUID', true);
$httpBackend.flush();
});
});
What I really want to do is to have my response object ( {"elements:...}) as the datas parameter of the fillMapDatas function. I don't understand how to mock all the service things (service, promise, then)
So you want to test, if your service responses as expected? Then, this is something you would rather test on the service. Unit test promise based methods could look like this:
var mapService, $httpBackend, $q, $rootScope;
beforeEach(inject(function (_mapService_, _$httpBackend_, _$q_, _$rootScope_) {
mapService = mapService;
$httpBackend = _$httpBackend_;
$q = _$q_;
$rootScope = _$rootScope_;
// expect the actual request
$httpBackend.expect('GET', '/onmap/rest/map/uuid?editor=true');
// react on that request
$httpBackend.whenGET('/onmap/rest/map/uuid?editor=true').respond({
success: {
elements: [1, 2, 3]
}
});
}));
As you can see, you don't need to use $injector, since you can inject your needed services directly. If you wanna use the correct service names throughout your tests, you can inject them with prefixed and suffixed "_", inject() is smart enough to recognise which service you mean. We also setup the $httpBackend mock for each it() spec. And we set up $q and $rootScope for later processing.
Here's how you could test that your service method returns a promise:
it('should return a promise', function () {
expect(mapService.getMapUuid('uuid', true).then).toBeDefined();
});
Since a promise always has a .then() method, we can check for this property to see if it's a promise or not (of course, other objects could have this method too).
Next you can test of the promise you get resolves with the proper value. You can do that setting up a deferred that you explicitly resolve.
it('should resolve with [something]', function () {
var data;
// set up a deferred
var deferred = $q.defer();
// get promise reference
var promise = deferred.promise;
// set up promise resolve callback
promise.then(function (response) {
data = response.success;
});
mapService.getMapUuid('uuid', true).then(function(response) {
// resolve our deferred with the response when it returns
deferred.resolve(response);
});
// force `$digest` to resolve/reject deferreds
$rootScope.$digest();
// make your actual test
expect(data).toEqual([something]);
});
Hope this helps!