I have a very strange problem with an angular app we are building. Whenever i load some data from a resource defined like below (simplest example I could build) I get some data back wich I can use for databinding (eg. ng-repeat='message in messages' or as {{message.id}}) however I can never read it from javascript by accessing it as an array or object (depending on wether i used get({id:myId}) or query()).
Iterating over it only gives me keys like $get, $query, $save, etc... but no actual data.
app.factory('Message', ['$resource', function($resource) {
return $resource('MY_URL/messages/:id', {id: '#id'});
}]);
app.service('messageService', ['$rootScope', 'Message', function($rootScope, Message) {
var messages = Message.query();
var selectedMessage = null;
var service = {};
service.get = function(id) {
// Problem A (see below for details)
if (arguments.length === 1) return Message.get({id: id});
else return messages;
};
var MenuCtrl = function($scope, Project, messageService, otherService) {
$scope.projects = Project.query();
$scope.messages = messageService.get();
// Problem B (details below)
};
At Problem A i want to be able to return a single element form a collection that has already been fetched, however i need some way to handle calls that happen before the data is ready.
At problem B i would like to process some of the fetched data and pass the result to "otherService" however i need a way to delay this until the data is ready.
I have only seen this issue come up in unit testing, and the way to get around it is by "flushing" the mock $httpBackend.
According to the API docs:
The $httpBackend used in production, always responds to requests with responses asynchronously. If we preserved this behavior in unit testing, we'd have to create async unit tests, which are hard to write, follow and maintain. At the same time the testing mock, can't respond synchronously because that would change the execution of the code under test. For this reason the mock $httpBackend has a flush() method, which allows the test to explicitly flush pending requests and thus preserving the async api of the backend, while allowing the test to execute synchronously.
Here's an example with some context:
// define a resource within a service
angular.module('app.services', ['ngResource'])
.factory('Message', function ($resource) {
var url = ...
, params = ...
, actions = ...;
return $resource(url, params, actions);
}
// Query the resource in a controller
function MessagesCtrl ($scope, $routeParams, Message) {
$scope.messages = Message.query();
}
// unit test with a mocked backend
describe('MessagesCtrl', function() {
var scope, ctrl, $httpBackend, messages;
beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) {
$httpBackend = _$httpBackend_;
scope = $rootScope.$new();
messages = [
{
id: '1',
text: 'foo',
}, {
id: '2',
text: 'foo',
}
]
$httpBackend.expectGET('/api/messages').respond(messages);
ctrl = $controller('MessagesCtrl', {$scope: scope});
}));
it('should get a list of messages', function () {
// THE NEXT LINE WILL SOLVE THE PROBLEM
$httpBackend.flush();
expect(scope.message).toEqualData(message);
});
});
Related
I have a controller that calls a Service which is a wrapper for a Resource. Like this:
app.factory("Service", ["$resource", function ($resource) {
return $resource("/api/Service/get");
}]);
Return value of the service's method is assigned to a variable within the controller. Normally, the variable is of type Resource and it contains a promise. When the promise is resolved, the variable is populated with all values returned from the backend.
I track then on the promise in order to modify the model received from the backend. Like so:
this.model = Service.get();
this.model.$promise.then(function(data) {
// do something with data
});
I need to test the value of the resulting model variable in my controller.
The only way I found to do this, is to use $httpBackend with a real implementation of my Service. However, this is ugly because then, testing my controller, I have to pass request path "api/Service/get" to the httpBackend.when() in order for it to respond with some value.
An excerpt form my test:
// call Controller
$httpBackend.when('GET', '/api/Service/get').respond(someData);
$httpBackend.flush();
expect(scope.model.property).toBe(null);
This seems and feels utterly wrong. The whole point of using a separate service to deal with resource is for the controller to not know anything about the url and http method name. So what should I do?
In other words, what I want to test is that then gets called and does what I need it to do.
I guess I could probably create a separate service that gets called in then and do what I need to do with the model but it feels a bit overkill if all I want to do is, for example, set one field to null depending on a simple condition.
You are correct, you shouldn't have to use $httpBackend unless you are using $http in the controller you are testing.
As you wrote, the controller shouldn't need to know anything about the implementation of Service. What the controller knows is that Service has a get method that returns an object with a $promise property that is a promise.
What you want to do is to use a fake implementation of Service in your test. There are multiple ways to do this via mocks, spies, stubs etc, depending on your use case and which testing framework(s) you are using.
One way is to create a fake implementation like this:
var Service = {
get: function() {
deferred = $q.defer();
return {
$promise: deferred.promise
};
}
};
You want to be able to access deferred from the tests, so you can either resolve or reject the promise based on what you want to test.
Full setup:
var $rootScope,
scope,
createController,
$q,
deferred;
var Service = {
get: function() {
deferred = $q.defer();
return {
$promise: deferred.promise
};
}
};
beforeEach(function() {
module('App');
inject(function(_$rootScope_, $controller, _$q_) {
$rootScope = _$rootScope_;
scope = $rootScope.$new();
createController = function() {
$controller('MyController', {
$scope: scope,
Service: Service
});
};
$q = _$q_;
});
});
Controller implementation:
app.controller('MyController', function($scope, Service) {
$scope.property = false;
$scope.model = Service.get();
$scope.model.$promise.then(function(data) {
if (data) {
$scope.property = true;
}
});
});
You can then spy on the fake implementation to assert that it is called correctly.
Example with Jasmine:
spyOn(Service, 'get').and.callThrough();
You need and.callThrough() or the call will be interrupted and your fake implementation will not be used.
You now have full control by manually creating the controller, resolving the promise and triggering the digest loop and can test the different states:
it('Should work', function() {
spyOn(Service, 'get').and.callThrough();
expect(Service.get).not.toHaveBeenCalled();
createController();
expect(Service.get).toHaveBeenCalled();
expect(scope.property).toBeFalsy();
deferred.resolve('some data');
$rootScope.$digest();
expect(scope.property).toBeTruthy();
});
Demo: http://plnkr.co/edit/th2pLWdVa8AZWOyecWOF?p=preview
I have one error in $http.json line.
Here is my code:
var myApp = angular.module('myApp', ["ionic"]);
myApp.service("Pressed", ["$http", "$log", Pressed]);
myApp.controller("AppCtrl", ["$scope", "Pressed", "$log", AppCtrl]);
function AppCtrl($scope, Pressed, $log){
$scope.refresh = function(){
Pressed.getBlogs();
}
}
function Pressed($http, $log) {
this.getBlogs = function() {
$http.jsonp('http://oasisgroups.com/oApp/product.php?callback=JSON_CALLBACK()')
.success(function(result){
$log.info(JSON.stringify(result.product));
});
};
}
When I click on refresh button, an error is displayed in the console:
You can also here find the respective service.
I believe the main issue is that the service doesn't seem to support a jsonp call. No matter how I call the service you provided it only responds with standard JSON results and not with the JSON wrapped in the callback function. Your screen shot of Chrome even shows the raw JSON, not JSONP response from the service. If a service doesn't support JSONP you can't force it to, that is something each service does on a case by case basis depending on how it is written. So the root cause of your error is that AngularJS is expecting the callback function to be part of the response, it cannot find it, and you get the error you are seeing.
I have constructed a jasmine test for your code and it passes. That is the best I can do to confirm that your code is correct and the issue is outside of your Angular code.
Unless the web service actually responds with the expected callback function wrapping the JSON, you need to switch to a standard $http.get() and deal with any potential XSS issues that you might encounter in a different way.
You can see a working JSONP example with this url. You will note that it starts with "getdata" and then wraps the JSON content inside that function's (). Your service is not doing that with the callback query string attribute.
var myApp = angular.module('myApp', []);
myApp.controller("AppCtrl", ["$scope", "Pressed", "$log", function ($scope, Pressed, $log) {
$scope.refresh = function () {
Pressed.getBlogs($scope);
}
}]);
myApp.service('Pressed', ['$http', '$log', function ($http, $log) {
var pressed = {};
pressed.getBlogs = function ($scope) {
$http.jsonp('http://oasisgroups.com/oApp/product.php?callback=JSON_CALLBACK')
.success(function (data,status,headers,config) {
$log.info(JSON.stringify(data));
$scope.products = data.product;
console.log('Found ' + data.product.length + ' products');
})
.error(function () {
console.log("Error during http get request.");
});
};
return pressed;
}]);
Then the test would look something like this:
describe('bad_jsonp', function () {
var service, scope;
beforeEach(module('myApp'));
beforeEach(angular.mock.inject(function ($rootScope) {
scope = $rootScope.$new();
}));
beforeEach(inject(function($httpBackend, _Pressed_) {
backend = $httpBackend;
service = _Pressed_;
}));
it('test that service response contains the attribute product', function () {
backend.expect("JSONP","http://oasisgroups.com/oApp/product.php?callback=JSON_CALLBACK").
respond(200,
{"success":1,"msg":"success","product":[{"image":"http:\/\/oasisgroups.com\/images\/oacgallery\/160112WP_20160112_17_57_49_Pro__1452604019_113.193.193.146.jpg","title":"Shreenath Ji"},{"image":"http:\/\/oasisgroups.com\/images\/oacgallery\/1601124e199090-c030-4f01-be11-c5140cf20273__1452603831_113.193.193.146.jpg","title":"Acrylic Jali"},{"image":"http:\/\/oasisgroups.com\/images\/oacgallery\/1601128a718e95-7df0-4189-876e-204b715cf90d__1452603868_113.193.193.146.jpg","title":"Acrylic Jali"},{"image":"http:\/\/oasisgroups.com\/images\/oacgallery\/16011229b095c9-b897-4942-831f-92073f527374__1452603895_113.193.193.146.jpg","title":"Wooden Decorative"},{"image":"http:\/\/oasisgroups.com\/images\/oacgallery\/16011255ce3155-3956-4cfb-8dd5-39021713d350__1452603914_113.193.193.146.jpg","title":"Acrylic Jali Oranage"},{"image":"http:\/\/oasisgroups.com\/images\/oacgallery\/160112WP_20160112_17_33_11_Pro__1452603994_113.193.193.146.jpg","title":"Acrylic Jali Green"},{"image":"http:\/\/oasisgroups.com\/images\/oacgallery\/160112607c733c-8dd5-442c-a584-6179339abb0e__1452603974_113.193.193.146.jpg","title":"Acrylic Jali White"},{"image":"http:\/\/oasisgroups.com\/images\/oacgallery\/160112300cca44-e783-48f7-b035-59ef0529ad53__1452603956_113.193.193.146.jpg","title":"Wooden Decorative"},{"image":"http:\/\/oasisgroups.com\/images\/oacgallery\/16011279e7c001-6663-4dfe-91ce-70cc87e6ca2d__1452603940_113.193.193.146.jpg","title":"Wooden Decorative"},{"image":"http:\/\/oasisgroups.com\/images\/oacgallery\/160112WP_20160112_17_58_35_Pro__1452604069_113.193.193.146.jpg","title":"Corian Design "},{"image":"http:\/\/oasisgroups.com\/images\/oacgallery\/160112WP_20160112_17_59_14_Pro__1452604098_113.193.193.146.jpg","title":"Corian Design "},{"image":"http:\/\/oasisgroups.com\/images\/oacgallery\/160112WP_20160112_18_00_34_Pro__1452604138_113.193.193.146.jpg","title":"AalaBuster"},{"image":"http:\/\/oasisgroups.com\/images\/oacgallery\/160112WP_20160112_18_01_20_Pro__1452604320_113.193.193.146.jpg","title":"AalaBuster"},{"image":"http:\/\/oasisgroups.com\/images\/oacgallery\/160112WP_20160112_18_02_08_Pro__1452604343_113.193.193.146.jpg","title":"Corian wash basin"},{"image":"http:\/\/oasisgroups.com\/images\/oacgallery\/160112WP_20160112_18_02_25_Pro__1452604370_113.193.193.146.jpg","title":"3d Corian Design"},{"image":"http:\/\/oasisgroups.com\/images\/oacgallery\/160112WP_20160112_18_02_43_Pro__1452604393_113.193.193.146.jpg","title":"3d Corian Design"},{"image":"http:\/\/oasisgroups.com\/images\/oacgallery\/160112WP_20160112_18_03_13_Pro__1452604424_113.193.193.146.jpg","title":"3d Wooden Decorative"}]}
);
expect(service).toBeDefined();
service.getBlogs(scope);
backend.flush();
console.log(scope.products);
var products = scope.products;
expect(products.length).toBe(17);
expect(products[0].title).toBe("Shreenath Ji");
});
});
The test doesn't include the actual callback in the response content because the mocking framework handles that wrapping and unwrapping for you just like AngularJS does in the first place, so it isn't an exact test but it is as close as I can get with what we have.
I am unit testing following framework with karma and jasmine.
var userApiFactory = (function ($http, $log, $q, appConfig) {
apiFactory.logoutUser = function () {
return $http.get(urlBase + "Auth/Logout");
};
}
I am 100% sure that API that actually gets hit through above $http request is working fine and returns a promise containing some JSON data.
I have not mocked factory because all of them contains methods making HTTP requests and by testing this factory I just have to test that all methods are being called with correct parameters and those methods are subsequently making correct http requests with correct parameters.
Hence, I have rather taken an instance of original factory and referenced it. I have also created fake httpBackEnd so that actual APIs are not hit rather a fake promise with required data is being returned. My unit test looks like this:
describe('unitTesting', function () {
var jsonData = {
"logoutSucess": true
}
beforeEach(angular.mock.module('sampleApp'));
//Mocking authService
beforeEach(angular.mock.module(function($provide){
$provide.constant('APP_CONFIG', {
apiBaseUrl: "https://some.url.com"
})
}));
var mock_appconfig, httpBackend, mockUserApiFactory;
beforeEach(angular.mock.inject(function(APP_CONFIG, $httpBackend, userApiFactory, $q){
mock_appconfig = APP_CONFIG;
httpBackend = $httpBackend;
mockUserApiFactory = userApiFactory;
var defer = $q.defer();
defer.resolve(jsonData);
httpBackend.when("GET",mock_appconfig.apiBaseUrl+"Auth/Logout").respond(defer.promise);
}));
it('mockUserApiFactory has log out method working', function(){
spyOn(mockUserApiFactory, "logoutUser");
var rcvd_data;
var res = mockUserApiFactory.logoutUser();
res.then(function(data){
rcvd_data = data;
})
expect(mockUserApiFactory.logoutUser).toHaveBeenCalled();
expect(rcvd_data).toEqual(jsonData);
})
});
I am getting error: Cannot read property then of undefined.
I'm looking to write a Jasmine unit test which executes a callback function passed to a then function. This then function is chained to a call to the AngularJS $http service, and it's inside a custom service. Here's the code I'm working with:
app.service('myService', function($rootScope, $http) {
var service = this;
var url = 'http://api.example.com/api/v1/resources';
service.resources = {
current: []
};
service.insertResource = function (resource) {
return $http.post(url, resource).then(function(response){
$rootScope.$broadcast('resources:updated', service.resources.current);
return response;
});
};
});
Here's my attempt to write a test which executes this callback, but to no avail:
describe('resource service', function() {
beforeEach(angular.mock.module('myapp'));
var resourceService;
beforeEach(inject(function(_resourceService_) {
resourceService = _resourceService_;
}));
it('should insert resources', function() {
resourceService.insertResource({});
});
});
There are several approaches you could take:
Use $httpBackend.expectPOST
Use $httpBackend.whenPOST
Move the code in the callback to a named function (not an anonymous one) and write a test for this function. I sometimes take this route b/c I don't want the trouble of writing tests with $httpBackend. I only test the callback function, I don't test that my service is calling the callback. If you can live w/that it's much simpler approach.
Check the documentation for $httpBackend for details. Here's a simple example:
describe('resource service', function() {
beforeEach(angular.mock.module('myapp'));
var resourceService, $httpBackend;
beforeEach(inject(function($injector) {
resourceService = $injector.get('resourceService');
$httpBackend = $injector.get('$httpBackend');
}));
afterEach(function() {
// tests will fail if expected HTTP requests are not made
$httpBackend.verifyNoOutstandingRequests();
// tests will fail if any unexpected HTTP requests are made
$httpBackened.verifyNoOutstandingExpectations();
});
it('should insert resources', function() {
var data: { foo: 1 }; // whatever you are posting
// make an assertion that you expect this POST to happen
// the response can be an object or even a numeric HTTP status code (or both)
$httpBackend.expectPOST('http://api.example.com/api/v1/resources', data).respond({});
// trigger the POST
resourceService.insertResource({});
// This causes $httpBackend to trigger the success/failure callback
// It's how you workaround the asynchronous nature of HTTP requests
// in a synchronous way
$httpBackend.flush();
// now do something to confirm the resource was inserted by the callback
});
});
I am very new to testing javascript. My application is using angularjs. I am using jasmine as a testing framework.
Here is the controller I am testing:
angular.module('logonController', ["ngval", "accountFactory"])
.controller("logonController", function logOnController(accountFactory, $scope, $window) {
$scope.hasServerError = false;
$scope.Logon = function () {
accountFactory.Logon($scope.data.LogOnModel)
.then(function (data) {
$window.location.href = "/";
},
function (data) {
$scope.hasServerError = true;
});
}
})
where accountFactory.Logon is making a Post request to the server.
What I want to test is when calling accountFactory.Logon:
On success - window.location.href is called
On error $scope.hasServerError is set to true
So far I have managed to do this:
"use strict";
describe("Logon Controller", function () {
var $scope, $location, $rootScope, $httpBackend, $controller, $window, createController;
beforeEach(function () {
module("logonController");
});
beforeEach(inject(function ($injector) {
$rootScope = $injector.get("$rootScope");
$scope = $rootScope.$new();
$location = $injector.get("$location");
$httpBackend = $injector.get("$httpBackend");
$controller = $injector.get("$controller");
$window = $injector.get("$window");
}));
beforeEach(function () {
createController = function () {
return $controller("logonController", {
"$scope": $scope,
});
};
$scope.data = {
LogOnModel: { username: "user", password: "pass" }
};
$window = { location: { href: jasmine.createSpy() } };
});
it("should redirect on successfull login", function () {
var controller = createController();
$httpBackend.whenPOST("/Account/Logon").respond(function (method, url, data, headers) {
return [200, {}, {}];
});
$scope.Logon();
$httpBackend.flush();
expect($window.location.href).toHaveBeenCalled();
});
});
My idea is to create a spy on $window.location.href and only check if it is called. But I am getting
Expected spy unknown to have been called.
As I said I am very new to testing javascript, so any help will be appreciated.
Sten Muchow's Answer is wrong in several aspects:
you can't specify a compound property name ("location.href") as 2nd parameter to spyOn. You have to give a real property name.
And even if you would do the spyOn correctly, andCallThrough() would still raise an exception, as $window.location.href is not a function which could be called through.
But he is still right in saying that you should not intermingle your controller test with the service test.
To answer the question:
The reason, that your expectation is not met (that even the spy still exists*) is, that you're doing the $window.location.href assignment inside a promise's then() function. That means, it will be executed asynchronously, namely AFTER your expect() call. To go around this, you would need to make your test work asynchronously (for how to do this I would like to advise you to the Jasmine documentation: http://jasmine.github.io/2.0/introduction.html).
* In accountFactory.Logon, by doing $window.location.href = (i.e. assignment) you will effectively overwrite your spy.
Even better solution:
Instead of manipulating $window.location.href, you should use $location.url().
$location is an Angular core service. You will benefit from the integration within the Angular application lifecycle (i.e. watchers will be automatically processed when the url changes) + it is seamlessly integrated with existing HTML5 APIs like History API: https://docs.angularjs.org/guide/$location
Then, you can spy on $location.url() as you would have spied on $window.location.href (if it had been a function).
You need to create a spy:
spyOn($window, 'location.href').andCallThrough();
But on a bigger note though, you shouldnt be testing the functionality of your service in the controller test.