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"]);
})
Related
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;
});
}]);
I have 2 controllers and one service:
angular.module('objDescApp', ['ui.router', 'ui.bootstrap']);
angular.module('objDescApp').config(['$stateProvider', '$urlRouterProvider', '$httpProvider', function ($stateProvider, $urlRouterProvider, $httpProvider) {
'use strict';
$urlRouterProvider.otherwise('/');
$stateProvider
.state('object', {
url: '/{name}',
views: {
"body": {
controller: 'ObjectDetailCtrl',
template: '...'
}
});
angular.module('objDescApp').controller('ObjectListCtrl', function ($scope, $state, ConfigService) {
ConfigService.getConfig(function(){//get config from server
$scope.object = ConfigService.fillConfigForObjList(); //use config
}
}
angular.module('objDescApp').controller('ObjectDetailCtrl', ['$scope', '$stateParams', 'ConfigService', function ($scope, $stateParams, ConfigService) {
$scope.current_object = ConfigService.fillConfigForObjDetail(); //use config
}
angular.module('objDescApp').factory('ConfigService', function ($http, $rootScope) {
var jsonConf;
var confTemplate = {"sometemplate" : {}};
function fillConfigForObjList (){
... //use jsonConf variable , and always wait for init because of called only inside callback function of getConfig();
};
function fillConfigForObjDetail(){
... //use jsonConf variable , but doesnt wait for jsonConf initialization, so error var 'is undefined' here.So I need to add some waiting for 'jsonConf' initialization logic here
};
return {
jsonConf: jsonConf,
fillConfigForObjDetail: fillConfigForObjDetail,
fillConfigForObjList: fillConfigForObjList,
getConfig: function(callback){
$http({
method: 'GET',
url: endPointUrl,
transformResponse: undefined
}).then(
function successCallback(response) {
jsonConf = JSON.parse(response.data);
$rootScope.getConfigError = false;
callback();
},
function errorCallback(response) {
if(response.status == "404"){
jsonConf = confTemplate;
}else{
console.log("Get config error");
jsonConf = confTemplate;
$rootScope.getConfigError = true;
}
callback();
}
);
}
}
So, when I load page with main path '/' everything is OK, because 'ObjectListCtrl' controller triggers getConfig() function which sets 'jsonConf' variable after response, so I can naviagte between any states and all works fine cause 'jsonConf' already setted;
But If I re-load page with starting path state like '/{name}' , so 'ObjectListCtrl' controller trigers 'getConfig()' request to server , but in async way 'ObjectDetailCtrl' controller was triggered and its $scope.current_object = ConfigService.fillConfigForObjDetail() expression, which throw jsonConf is undefined error;
So could someone tell me how I can wait inside 'fillConfigForObjDetail()' function till 'jsonConf' variable init by getConfig() function.
Save the promise and chain from the promise.
var jsonConfPromise;
function getConfig() {
//save httpPromise
jsonConfPromise = $http({
method: 'GET',
url: endPointUrl,
transformResponse: undefined
}).then(
function successCallback(response) {
jsonConf = JSON.parse(response.data);
$rootScope.getConfigError = false;
//return for chaining
return jsonConf;
},
function errorCallback(response) {
if(response.status == "404"){
jsonConf = confTemplate;
}else{
console.log("Get config error");
jsonConf = confTemplate;
$rootScope.getConfigError = true;
}
//return for chaining
return jsonConf;
}
);
//return promise
return jsonConfPromise;
};
Your function that had problems can then chain from the promise.
function fillConfigForObjDetail(){
/*
... //use jsonConf variable , but doesnt wait for jsonConf
initialization, so error var 'is undefined' here.
So I need to add some waiting for 'jsonConf' initialization
logic here
*/
//chain from the promise
jsonConfPromise.then(function (jsonConf) {
//use resolved jsonConf
});
};
Also in your controller instead of using a callback, chain from the returned promise.
angular.module('objDescApp').controller('ObjectListCtrl', function ($scope, $state, ConfigService) {
var configPromise = ConfigService.getConfig();
configPromise.then(function() {
$scope.object = ConfigService.fillConfigForObjList();
});
});
The AngularJS framework uses promises instead of callbacks.
For more information on chaining promises, see AngularJS $q Service API Reference -- chaining promises.
Because calling the then method of a promise returns a new derived promise, it is easily possible to create a chain of promises. It is possible to create chains of any length and since a promise can be resolved with another promise (which will defer its resolution further), it is possible to pause/defer resolution of the promises at any point in the chain. This makes it possible to implement powerful APIs.1
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
I tried to mocking angular promises but I got some errors like undefined is not a function evaluating 'spyOn' fileUploadService
My controller code is
$scope.getUserFiles = function() {
fileUploadService.retrieveUserFileNames('')
.then(function(data) {
$scope.userFiles = data;
});
};
service code, I call this method from my controller
this.retrieveUserFileNames= function(userId) {
var deferred = $q.defer();
$http({
method : "GET",
url : "/retrieveExcelSheetNames",
params : {
"userId" : userId
}
}).success(function(data) {
deferred.resolve(data);
}).error(function(data, status) {
deferred.reject(data);
});
return deferred.promise;
};
test controller code
beforeEach(function() {
inject(function(_fileUploadService_ , _$q_) {
spyOn(scope, '$on');
var deferred = _$q_.defer();
fileUploadService = _fileUploadService_;
deferred.resolve('resolveData');
spyOn(fileUploadService, 'retrieveUserFileNames').andReturn(deferred.promise);
});
});
it('is now a lot easier', function() {
scope.getUserFiles();
rootScope.$apply();
expect(scope.userFiles).toBe('resolveData');
});
Thanks
As you're asynchronously set $scope.userFiles in your controller so you need to wait for the $apply from $http that resolved your promise.
Assuming you're using Jasmine you could use the asynchronous spec notation and register a $watch function to test your controller (Didn't test the code so you might need to tune it and it also depends on other modifications of the scope variable).
it('is now a lot easier', function(done) {
scope.$watch('userFiles', function() {
expect(scope.userFiles).toBe('resolveData');
// Call done function to tell Jasmine that the spec has completed
done();
});
scope.getUserFiles();
});
However, I think you should rather test your service and you can use $httpBackend from ngMock which is overriding $http and enables you to test much more in-depth. There you can test / spy the calls to the mock backend and also use Jasmines done() function to wait for the promise.
Here is a simple exmaple:
angular.module('mainApp', [])
// Simple factory that uses $http for later use of $httpBackend mocking structure
.factory('userManager', function($http, $q) {
return $http.get('/api/users');
});
And the specs:
describe('Simple factory that uses $http for later use of $httpBackend mocking structure', function() {
// We need to use the module mainApp for all our tests
beforeEach(module('mainApp'));
// We use $httpBackend from ngMock module to mock our webservice call ($http will be overriden)
// Also we need to inject inside of spec function as we need to use the async spec form with
// a done function and this is not available using the inject to proxy the spec function
it('should return desired user list', function(done) {
inject(function($httpBackend, userManager) {
$httpBackend.when('GET', '/api/users').respond({
userList: [
{user: 'User A'},
{user: 'User B'},
{user: 'User C'},
{user: 'User D'},
]
});
$httpBackend.expectGET('/api/users');
// userManager is returning a promise so we need to check asynchronously
userManager.getUserList().then(function(result) {
expect(result.data.userList).toContain(
{user: 'User A'},
{user: 'User B'},
{user: 'User C'},
{user: 'User D'}
);
// This call to the Jasmine done function is very important for asynchronous
// unit tests as Jasmine is determining if the test is done by this call
done();
});
$httpBackend.flush();
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
});
});
Im writing some unit tests for my controller which uses promises.
Basically this:
UserService.getUser($routeParams.contactId).then(function (data) {
$scope.$apply(function () {
$scope.contacts = data;
});
});
I have mocked my UserService. This is my unit test:
beforeEach(inject(function ($rootScope, $controller, $q, $routeParams) {
$routeParams.contactId = contactId;
window.localStorage.clear();
UserService = {
getUser: function () {
def = $q.defer();
return def.promise;
}
};
spyOn(UserService, 'getUser').andCallThrough();
scope = $rootScope.$new();
ctrl = $controller('ContactDetailController', {
$scope: scope,
UserService:UserService
});
}));
it('should return 1 contact', function () {
expect(scope.contacts).not.toBeDefined();
def.resolve(contact);
scope.$apply();
expect(scope.contacts.surname).toEqual('NAME');
expect(scope.contacts.email).toEqual('EMAIL');
});
This give me the following error:
Error: [$rootScope:inprog] $digest already in progress
Now removing the $scope.$apply in the controller causes the test to pass, like this:
UserService.getUser($routeParams.contactId).then(function (data) {
$scope.contacts = data;
});
However this breaks functionality of my controller... So what should I do here?
Thanks for the replies, the $apply is not happening in the UserService. It's in the controller. Like this:
EDIT:
The $apply is happening in the controller like this.
appController.controller('ContactDetailController', function ($scope, $routeParams, UserService) {
UserService.getUser($routeParams.contactId).then(function (data) {
$scope.$apply(function () {
$scope.contacts = data;
});
});
Real UserService:
function getUser(user) {
if (user === undefined) {
user = getUserId();
}
var deferred = Q.defer();
$http({
method: 'GET',
url: BASE_URL + '/users/' + user
}).success(function (user) {
deferred.resolve(user);
});
return deferred.promise;
}
There are a couple of issues in your UserService.
You're using Q, rather than $q. Hard to know exactly what effect this has, other than it's not typical when using Angular and might have affects with regards to exactly when then callbacks run.
You're actually creating a promise in getUser when you don't really need to (can be seen as an anti-pattern). The success function of the promise returned from $http promise I think is often more trouble than it's worth. In my experience, usually better to just use the standard then function, as then you can return a post-processed value for it and use standard promise chaining:
function getUser(user) {
if (user === undefined) {
user = getUserId();
}
return $http({
method: 'GET',
url: BASE_URL + '/users/' + user
}).then(function(response) {
return response.data;
});
}
Once the above is changed, the controller code can be changed to
UserService.getUser($routeParams.contactId).then(function (data) {
$scope.contacts = data;
});
Then in the test, after resolving the promise call $apply.
def.resolve(contact);
scope.$apply();