How do I test a request being sent and data is being received with a JSONP service?
angular.module('search', [])
.factory('SearchService', function($q,$rootScope,$resource) {
var _search = {};
_search.user = function(opts){
return $resource('https://api.github.com/users/:user', {user: opts.user}, {
search: {method:'JSONP',params:{callback: 'JSON_CALLBACK'}}
});
}
return _search;
});
Following the GET request examples:
describe('search tests', function () {
var svc, httpBackend;
beforeEach(function (){
module('ngResource');
module('search');
inject(function($httpBackend, SearchService) {
svc = SearchService;
httpBackend = $httpBackend;
});
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
httpBackend.verifyNoOutstandingRequest();
});
it('should send the message and return the response', function (){
var returnData = { testing: 'anything'};
httpBackend.expectJSONP('https://api.github.com/users/gigablox?callback=JSON_CALLBACK').respond(returnData);
svc.user({user:'gigablox'}, function(user) {
expect(user.testing).toEqual('anything');
});
httpBackend.flush();
});
});
I can't seem to get by some errors:
Error: No pending request to flush !
Error: Unsatisfied requests: JSONP
Using AngularJS 1.2 and Karma 0.10.2
Edit
Got it working here: http://jsfiddle.net/DhUqT/
Try to call your search like the following:
svc.user({user:'gigablox'}).search(function(user) {
expect(user.testing).toEqual('anything');
});
The reason is the call to svc.user() returns a resource class object with a search() method. The search() method uses jsonp to make request. Without calling the search() method, $httpBackend will not see any request, so you see the errors mentioned in your question.
Related
I am trying to write a code to test the service call which is done in my controller . I am trying to unit test that particular function in controller which is doing the service call and bringing the data . Currently i am trying with local json , but it will actually do a service call .
I got to know that first i have to create a spy object but i am getting the error, my goal is to successfully unit test the http call happening in the controller.
i am new to unit testing .Pasting my code , request you to help me please , struggling in this from many days now.Also i have gone through many solutions , they are so different making be confused.Your help is greatly appreciated
Service code :
//xlpApp is the module name
xlpApp.factory('xlpServices', ['$rootScope', '$http', function($rootScope,
$http) {
var xlpServices = {};
xlpServices.getProgramData = function(){
return $http.get(scripts/services/data/unitTesting.json');
};
unitTesting.json code :
{
"name":"unit testing"
}
Controller Code :
$scope.events = {
programData: function(){
xlpServices.getProgramData().then(function(response) {
if (response && response.data) {
$scope.testVariable= response.data.name;
}
});
},
selectSortingType : function(type) {
$scope.selectedSorting = type;
selectedFilter.sortingBy = $scope.selectedSorting;
}
}
$scope.events.programData();
Unit Test Code :
describe('myProgramGpmCtrl', function() {
beforeEach(module('xlp'));
var $controller;
beforeEach(inject(function(_$controller_){
$controller = _$controller_;
}));
describe('service call test', function() {
var xlpServices , myService , $q;
var $scope = {};
beforeEach(inject(function(xlpServices,_$q_){
xlpServices = xlpServices;
$q = _$q_;
var controller = $controller('myProgramGpmCtrl', { $scope: $scope });
myService = jasmine.createSpyObj('xlpServices',['getProgramData']);
}));
it('Service call test ', function() {
expect(myService.getProgramData).toHaveBeenCalled();
});
});
});
ERROR :
Expected spy xlpServices.getProgramData to have been called.
Try something like,
describe('service call test', function() {
var xlpServicesMock , myService , $q;
var $scope = {};
beforeEach(inject(function(xlpServices,_$q_){
xlpServicesMock = xlpServices;
$q = _$q_;
spyOn(xlpServicesMock ,'getProgramData').and.callFake(function() {
// we can return promise instead this custom object
return {
then: (callback, errorCallback) => {
callback('data to be passed to then callback');
/* `callback` function should be invoked when you wanted to test the service for success response with status code 200.
`errorCallback` function should be invoked with 500 status code when you wanted to test the api failure
Ex: callback({status: 200, data: <your data here>);
errorCallback({status: 500, data: <your error data>})
You can prepare the response to be passed as you wanted.
*/
}
};
});
var controller = $controller('myProgramGpmCtrl', { $scope: $scope, xlpServices: xlpServicesMock });
}));
it('Service call test ', function() {
$scope.events.programData();
expect(myService.getProgramData).toHaveBeenCalled();
});
});
There are good resources available online.
check here and here
Hii I m using following code. I am reading a json file name is "users.json". If i read this file in controller through $http everything works fine. but i want to use the data that i read from file, again and again in different controller so i made a factory for this. but in factory when i read data from that json file through $http.get() and in return when i call that service method in my controller and it returns Object { $$state: Object }
app.factory('AboutFactory',['$http',function ($http) {
return {
getter: function () {
return $http({
method : 'GET',
url : '/home/penguin/Modeles/users.json',
cache : true
})
.then(function (response) {
return response.data
})
}
}
}])
Result of getter function is a promise. so you should use it like this:
AboutFactory.getter().then(function(res)
{
console.log(res);
});
That's because the $http service returns a promise as mentioned in the documentation:
The $http API is based on the deferred/promise APIs exposed by the $q
service. While for simple usage patterns this doesn't matter much, for
advanced usage it is important to familiarize yourself with these APIs
and the guarantees they provide.
You can think of a promise as if you give a top secret message to someone to deliver personally to a friend, then when delivered, report back to you with a message back from your friend.
You provide the message (the request object) to the person so that they can attempt to make the delivery of the message (send the request).
The attempted delivery has taken place (the request has been sent), it either:
a) was delivered successfully (successful response) or
b) your friend was not in so the letter could not be delivered (non success response).
You can then act depending on the response you get back
a) Message was delivered (it was a successful request) and you got a letter back (do something with the response) or
b) Message failed to get delivered (request wasn't successful), so you can maybe try again later or do something else as you don't have the information you requested
Here is an example of using the $http service with the $q service:
// app.js
(function() {
'use strict';
angular.module('app', []);
})();
// main.controller.js
(function() {
'use strict';
angular.module('app').controller('MainController', MainController);
MainController.$inject = ['AboutFactory'];
function MainController(AboutFactory) {
var vm = this;
AboutFactory.getter().then(function(data) {
// do something with your data
vm.data = data;
}, function(error) {
// give the user feedback on the error
});
}
})();
// about.service.js
(function() {
'use strict';
angular.module('app').factory('AboutFactory', AboutFactory);
AboutFactory.$inject = ['$http', '$q']
function AboutFactory($http, $q) {
var service = {
getter: getter
};
return service;
function getter() {
// perform some asynchronous operation, resolve or reject the promise when appropriate.
return $q(function(resolve, reject) {
$http({
method: 'GET',
url: 'https://httpbin.org/get',
cache: true
}).then(function(response) {
// successful status code
// resolve the data from the response
return resolve(response.data);
}, function(error) {
// error
// handle the error somehow
// reject with the error
return reject(error);
});
});
}
}
})();
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.1/angular.min.js"></script>
<div ng-app="app" ng-controller="MainController as MainCtrl">
<pre>{{MainCtrl.data | json}}</pre>
</div>
Try this approach. It will work as per your expectation.
Read JSON file in controller through $http service as it is works fine.
For sharing the response data from one controller to another you can create a service and store the data into that service.
Service :
app.service('setGetData', function() {
var data = '';
getData: function() { return data; },
setData: function(requestData) { data = requestData; }
});
Controllers :
app.controller('myCtrl1', ['setGetData',function(setGetData) {
// To set the data from the one controller
var data = 'Hello World !!';
setGetData.setData(data);
}]);
app.controller('myCtrl2', ['setGetData',function(setGetData) {
// To get the data from the another controller
var res = setGetData.getData();
console.log(res); // Hello World !!
}]);
Here, we can see that myCtrl1 is used for setting the data and myCtrl2 is used for getting the data. So, we can share the data from one controller to another controller like this.
I'm trying to learn how to use Angular right, by having all my business logic in services.
When I do a post request in a service, I get the following error:
Cannot read property 'post' of undefined
Here is some code:
UrlApp.controller('UrlFormCtrl', UrlFormCtrl);
UrlApp.factory('addUrlService', addUrlService);
function UrlFormCtrl($scope, $http) {
console.log('Url Form Controller Initialized');
$scope.addUrl = addUrlService.bind(null, $http);
}
function addUrlService($scope, $http){
console.log('initializing addUrlService');
return $http.post('urls/create', {'test':'test'}).then(function(response){
return response.data;
});
}
I'm just getting the hang of Angular, so I'm not entirely sure what I'm doing wrong. See any problems?
Firstly, you don't need to inject $scope in your service.
Secondly, you don't need to inject $http service in your controller.
Thirdly, you need to inject the service in your controller.
Finally, addUrlService service is returning a promise meaning it will make a request when service is instantiated. You may want to return a function instead or an object containing several functions.
So I would change your code to:
UrlApp.controller('UrlFormCtrl', UrlFormCtrl);
UrlApp.factory('AddUrlService', AddUrlService);
function UrlFormCtrl($scope, AddUrlService) {
$scope.addUrl = AddUrlService.addUrl;
}
function AddUrlService($http) {
function addUrl() {
return $http.post('urls/create', {
'test': 'test'
}).then(function (response) {
return response.data;
});
}
return {
addUrl: addUrl
};
}
Can you try like this
UrlApp.controller('UrlFormCtrl', UrlFormCtrl);
UrlApp.factory('addUrlService', addUrlService);
function UrlFormCtrl($scope,addUrlService) {
console.log('Url Form Controller Initialized');
$scope.addUrl = addUrlService;
}
function addUrlService($http){
console.log('initializing addUrlService');
return $http.post('urls/create', {'test':'test'}).then(function(response){
return response.data;
});
}
I am making a simple get request using $http .and it fails with 503-service unavailable error which is clearly shown in the network tab .But the rejection object in the responseError interceptor object shows status: 0.
Here is my Interceptor
angular.module("app", []).config(function($httpProvider){
$httpProvider.interceptors.push(function($q) {
return {
'responseError': function(rejection) {
console.log(rejection);
}
}
})
});
I am expecting status code 503 in the interceptor but i am getting 0.Please help me to understand and resolve the issue.
Here is a fiddle illustrating the issue.
The request is a cross domain request.
The service you are using does return any data. Angular $http expects some data to be returned, and in this case "GetStatusCode" just stops and does not return anything.
Also, fyi - your fiddle does not respect angular's method of binding to a controller, which expects a string for the controller name.
See http://plnkr.co/edit/x46D5FIsgaKsaSocYjpz?p=preview for proper markup.. where we name the controller as a string, for ex:
angular.module("plunker", [])
.config(function ($httpProvider) {
$httpProvider.interceptors.push(function () {
return {
'responseError': function (rejection) {
console.log('rejection = ', rejection);
},
'response': function (response) {
console.log('response = ', response);
}
}
})
})
.controller('Controller', function Controller($scope, $http) {
$scope.getCode = function () {
var req = 'http://www.reddit.com/r/catpictures.json?limit=50&jsonp=JSON_CALLBACK';
return $http.jsonp(req);
};
$scope.get = function () {
return $scope.getCode()
};
})
;
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
});
});
});
}());