I would like to know how to unit test AngularJs $resource query with params.
I have this:
services.js
angular.module('conciergeApp.services', ['ngResource'])
.factory('Reply', function($resource) {
return $resource('/api/reply/:id/');
});
controllers.js
conciergeControllers.controller('ReplyCtrl', ['$scope', 'Reply',
function($scope, Reply) {
$scope.hotel_replies = Reply.query({
hotel: $scope.hotel_id
});
}
]);
So, the API Endpoint that gets queried here is:
'/api/reply/?hotel=<hotel_id>'
// aka
'/api/reply/?hotel=1'
This works when manually tested, but how do you unit test this?
With Jasmine spyOn, or $httpBackend. Whichever would be fine?
Can this be used? I tried this pattern, but couldn't do it successfully:
spyOn(object, 'method').andCallFake(function({foo:bar}) {
return myMockObject
});
Example here using Jasmine 2.0 and htmlRunner. Explanation to follow:
So inside your ReplyCtrl unit test:
describe('ReplyCtrl Controller', function () {
var Reply;
var ReplyCtrl;
var $scope;
var ReplyQueryMock;
var HOTEL_ID = 1234;
beforeEach(module('conciergeApp'));
beforeEach(inject(function (_Reply_, $controller, $rootScope) {
// This object can be used throughout your controller unit test for the
// response of calling the Reply.query method
ReplyQueryMock = {};
Reply = _Reply_;
spyOn(Reply, 'query').and.returnValue(ReplyQueryMock);
$scope = $rootScope.$new();
$scope.hotel_id = HOTEL_ID;
ReplyCtrl = $controller('ReplyCtrl', {$scope: $scope});
}));
it('should populate hotel_replies', function () {
expect(Reply.query).toHaveBeenCalledWith({
hotel: HOTEL_ID
});
expect($scope.hotel_replies).toBe(ReplyQueryMock);
});
});
This will mock out your Reply service and return a mocked object for the query. You will verify that your $scope's hotel_replies is the result of calling Reply.query with the hotel_id that is present on $scope.
Edit: Presumably $resource has been unit tested for you (by Angular team!). You might just want to verify that you setup the resource with the correct URL rather than digging down into the internals of what $resource really does (like calling $http for example). Fiddle link above updated, and would look something like the following:
describe('Reply Resource', function () {
var Reply;
var $resource;
var ReplyResourceMock;
beforeEach(module('conciergeApp.services', function ($provide) {
// Mock out $resource here
$provide.factory('$resource', function () {
ReplyResourceMock = {};
var $resource = jasmine.createSpy('$resource');
$resource.and.returnValue(ReplyResourceMock);
return $resource;
});
}));
beforeEach(inject(function (_Reply_, _$resource_) {
Reply = _Reply_;
$resource = _$resource_;
}));
it('should initialize resource', function () {
expect($resource).toHaveBeenCalledWith('/api/reply/:id/');
expect(Reply).toBe(ReplyResourceMock);
});
});
From original answer, probably overkill since you use $resource to abstract $http (still shown in fiddle)
Then to verify $resource works correctly from your Reply factory/service, you will want to inject $httpBackend. You can verify that the proper REST endpoint is called with the correct data.
Related
I have a WebAPI service that returns dynamic configuration data. Before my angular app loads I would like to call that service and load the config data into angular. JSFiddle of my attempt at doing that. My question is, after seeing the string test in the console I am seeing nothing else written into the console. How do I get test 2 and wierd wierd to appear into the console
var app = angular.module('app', [])
app.provider("ConfigService", function () {
var self = this;
self.Settings = {};
self.config = function (data) {
console.log(data);
};
this.$get =
function($http) {
return self;
};
});
angular.element(document).ready(function($http) {
console.log('test')
angular.module('app').config([
'ConfigServiceProvider',
function(configService) {
console.log('test 2')
$http.get('http://www.google.com').then(function(result) {
console.log('wierd wierd')
configService.config(result);
angular.bootstrap(document, ['app']);
})
}
]);
});
EDIT
In response to the question, why I do not run this in app.run phase instead.
In the app.run phase the app is still initializing and sometimes it loads up prior to my configuration section being completed. I wanted 100% guarantee that my config section is loaded first before any of the app is.
You can use $http outside of your angular module with angular.injector. With $http you can request the config from your server and bootstrap your app when $http's promise resolves.
JS Fiddle
Create module
var app = angular.module("app", []);
app.provider("configService", function () {
var configService = {
config: {}
};
this.setConfig = function (config) { configService.config = config; };
this.$get = function() { return configService; };
});
Function that fetches config from server
function fetchConfig() {
var $http = angular.injector(["ng"]).get("$http");
return $http.get("http://www.google.com");
}
Function that bootstraps app
function bootstrap(config) {
app.config(["configServiceProvider", function (configServiceProvider) {
configServiceProvider.setConfig(config);
}]).run(["configService", function (configService) {
//Not necessary, just to confirm everything worked
console.log("YAY! You have a config:", configService.config);
}]);
angular.bootstrap(document, ["app"])
}
Put it all together!
fetchConfig().then(
/*sucess*/function (config) { angular.element(document).ready(function () { bootstrap(config); }); },
/*fail*/ function (err) { console.log("UH OH could not retrieve config!", err); });
EDIT: Please use #StevenWexler 's answer: https://stackoverflow.com/a/37599857/5670592. It is much more correct, uses a nifty angular feature ($inject), and will provide configuration before the beginning of the bootstrap cycle.
I have updated the application with your constraints regarding blocking execution until API call is complete.
Try this: https://jsfiddle.net/6svnemu8/3/
I moved the code to the module.run(...) block. This is where all providers are available and you can use $http and your ConfigService. I kept the bootstrap call in the document ready function, and I also added the $q service so you can block execution of the application until the API call is complete. You can verify this by looking at the order of the test outputs in the console:
angular.module('app').run([
'ConfigService', '$http', '$q',
function(configService, $http, $q) {
console.log('test 2');
var deferred = $q.defer();
$http.get('/6svnemu8/2/').then(function(result) {
deferred.resolve(result);
}, function(result){
deferred.reject(result);
});
console.log("test 3");
deferred.promise.then(function(result){
console.log('wierd wierd');
configService.config(result);
}, function(result){
console.log("call failed.");
});
}
]);
Option 1 -- if you have an MVC app
In your main razor view, use JSON.Net to serialize your Model (or a property on it) to JavaScript.
<script>
window.configuration = #(Html.Raw(JsonConvert.SerializeObject(Model)))
</script>
Then put it into an angular constant so you can inject it anywhere you need it, and it's guaranteed to be there. This is the most convenient way to do it.
angular.module('YourModule').constant('configuration', window.configuration);
Option 2 -- loading it asynchronously
This service will load the configuration and cache the promise.
angular.module('YourModule').factory('configuration', ['$http', function($http) {
var configurationLoaded;
var service = {
get: get
};
function get() {
if(configurationLoaded) return configurationLoaded;
configurationLoaded = $http.get( ... );
return configurationLoaded;
}
return service;
}]);
Then anywhere you need it, you'll have to pull out properties from it like this:
angular.module('YourModule').controller('SomeController', ['configuration', function(configuration) {
var vm = this;
configuration.get().then(function(config) {
vm.someSetting = config.someSetting;
});
}]);
I've checked several solutions on the web but I don't quite understand how to stop the controllers from loading. So, I've created a plunkr to highlight what I want to do.
Basically: I want to load all data in a service and then pass around that data from that service to each controller. When the app first loads, because it's Async, the controllers are loaded first.
I could have just used the factory in each controller, but I want to hold that data in the "allProducts" property of the service. I don't see the need to call the factory function each time a view loads.
In the example, I've also tried with the $q service, but seems to me it has the same behaviour just like the http from the factory and still needs to call the http request on each view load...
So, could somebody help me with this example and implement an elegant solution?
app.factory('productsFactory', ['$http', '$q',
function($http, $q) {
var cachedData; //here we hold the data after the first api call
function getData(callback) {
if (cachedData) {
callback(cachedData);
} else {
$http.get('http://api.bestbuy.com/v1/products(longDescription=iPhone*|sku=7619002)?show=sku,name&pageSize=15&page=5&apiKey=bqs7a4gwmnuj9tq6bmyysndv&format=json')
.success(function(data) {
cachedData = data; //caching the data in a local variable
callback(data);
});
}
}
return {
getProds: getData
}
}
])
app.service('appService', ['productsFactory', '$q',
function(productsFactory, $q) {
var _this = this;
productsFactory.getProds(function(data) {
_this.allProducts = data; //wait for data to load before loading the controllers
})
}
])
app.controller('productsCtrl', ['$scope', 'appService',
function($scope, appService) {
$scope.myProducts = appService.allProducts;
}
]);
plunkr here: http://plnkr.co/edit/ZvtYwXHSasC3fCAZKkDF?p=preview
I didn't actually test it, but looks like you need to create and return a promise in order to have the data returned when it's available.
app.factory('productsFactory', ['$http', '$q',
function($http, $q) {
var cachedData; //here we hold the data after the first api call
function getData(callback) {
var d = $q.defer();
if (cachedData) {
d.resolve(callback(cachedData));
} else {
$http.get('http://api.bestbuy.com/v1/products(longDescription=iPhone*|sku=7619002)?show=sku,name&pageSize=15&page=5&apiKey=bqs7a4gwmnuj9tq6bmyysndv&format=json')
.success(function(data) {
cachedData = data; //caching the data in a local variable
d.resolve(callback(cachedData));
});
}
return d.promise;
}
return {
getProds: getData
}
}
])
app.service('appService', ['productsFactory', '$q',
function(productsFactory, $q) {
var _this = this;
productsFactory.getProds(function(data) {
_this.allProducts = data; //wait for data to load before loading the controllers
})
}
])
app.controller('productsCtrl', ['$scope', 'appService',
function($scope, appService) {
$scope.myProducts = appService.allProducts;
}
]);
Check this: http://plnkr.co/edit/ey0na3l2lyT0tUdbDyrf?p=preview
Changes:
- a main app controller that wraps all your app.
In this controller we prevent route change if the boot hasn't finished all its jobs. When boot is finished, we change location to the main/default route.
- in our run block we set the bootStatus var to false and wait until products are fetch.
Also, I've stored the result from service to $rootScope so you can use that data in all your controllers without injecting the service over and over again.
FINALLY!!! I Managed to do what I was looking for.
Note, this is not necessarily a best practice, but it was a problem that was bugging me and wanted to know how it's done.
The way I did it was to create a global variable, where I use a main resolve function to wait for the factory to do the http get and then pass it to the service. Then, I use resolve on every state where I need that data and reference that function.
UPDATE: Realized that I was calling the same factory function each time the state was changins, so I decided to go with a variable - a property in the appService which turns to true when the http get was called once: appService.retrieved and changed the main resolve function a bit.
var mainResolve = ['$q', 'appService', 'productsFactory', function($q, appService, productsFactory) {
var defer = $q.defer();
if(appService.retrieved) {
defer.resolve();
} else {
productsFactory.getProds(function(data) {
appService.allProducts = data;
defer.resolve();
})
appService.retrieved = true;
}
return defer.promise;
}]
And in the state
.state('home', {
url: "/home",
templateUrl: "home.html",
controller: 'homeCtrl',
resolve: {
waitingFor: mainResolve
}
})
You can find the plnkr with the working solution, here: http://plnkr.co/edit/ZvtYwXHSasC3fCAZKkDF?p=preview
Again, the factory could be refactored some more and eliminate some code like the caching data. This way, we only go once to the factory function.
I'm looking for some information on the best way to retrieve data from a local JSON file and handle the response. After browsing through Stack Overflow, I have some mixed thoughts as I've seen multiple ways of doing the same thing (although no explanation on why one may or may not be preferred).
Essentially, I have an Angular app that is utilising a factory to retrieve data from a JSON file; I'm then waiting for the response to resolve in my controller before using it in my html file, similar to the below:
Option 1
Factory:
comparison.factory('Info', ['$http', function($http) {
var retrievalFile = 'retrievalFile.json';
return {
retrieveInfo: function() {
return $http.get(retrievalFile);
}
}
}]);
Controller:
comparison.controller('comparisonController', ['$scope', 'Info', function($scope, Info) {
Info.retrieveInfo().then(function(response) {
$scope.info = response.data;
});
}]);
My main point of contention is figuring out when it's best to wait for the response to resolve, or if it even matters. I'm toying with the idea of having the factory return the fulfilled promise, and wait for the controller to retrieve the data also. In my view, it's best to abstract all data retrieval out of the controller and into the factory, but I'm not sure if this extends to waiting for the actual data to be returned within the factory itself. With this in mind, I'm confused about whether to opt for option 1 or option 2 and would really appreciate some feedback from more experienced/qualified developers!
Option 2
Factory:
comparison.factory('Info', ['$http', function($http) {
var retrievalFile = 'retrievalFile.json';
return {
retrieveInfo: function() {
return $http.get(retrievalFile).then(function(response) {
return response.data;
});
}
}
}]);
Controller:
comparison.controller('comparisonController', ['$scope', 'Info', function($scope, Info) {
Info.retrieveInfo().then(function(response) {
$scope.info = response;
});
}]);
Thank you for any input/suggestions in advance!
It depends on what your controller is expecting and how you set up your application. Generally, I always go with the second option. Its because I usually have global error or success handlers in all api requests and I have a shared api service. Something like below.
var app = angular.module('app', []);
app.service('ApiService', ['$http', function($http) {
var get = function(url, params) {
$http.get(url, { params: params })
.then(handleSuccess, handleError);
};
// handle your global errors here
// implementation will vary based upon how you handle error
var handleError = function(response) {
return $q.reject(response);
};
// handle your success here
// you can return response.data or response based upon what you want
var handleSuccess = function(response) {
return response.data;
};
}]);
app.service('InfoService', ['ApiService', function(ApiService) {
var retrieveInfo = function() {
return ApiService.get(retrievalFile);
/**
// or return custom object that your controller is expecting
return ApiService.get.then(function(data) {
return new Person(data);
});
**//
};
// I prefer returning public functions this way
// as I can just scroll down to the bottom of service
// to see all public functions at one place rather than
// to scroll through the large file
return { retrieveInfo: retrieveInfo };
}]);
app.controller('InfoController', ['InfoService', function(InfoService) {
InfoService.retrieveInfo().then(function(info) {
$scope.info = info;
});
}])
Or if you are using router you can resolve the data into the controller. Both ngRouter and uiRouter support resolves:
$stateProvider.state({
name: 'info',
url: '/info',
controller: 'InfoController',
template: 'some template',
resolve: {
// this injects a variable called info in your controller
// with a resolved promise that you return here
info: ['InfoService', function(InfoService) {
return InfoService.retrieveInfo();
}]
}
});
// and your controller will be like
// much cleaner right
app.controller('InfoController', ['info', function(info) {
$scope.info = info;
}]);
It's really just preference. I like to think of it in terms of API. What is the API you want to expose? Do you want your controller to receive the entire response or do you want your controller to just have the data the response wraps? If you're only ever going to use response.data then option 2 works great as you never have to deal with anything but the data you're interested in.
A good example is the app we just wrote where I work. We have two apps: a back-end API and our front-end Angular application. We created an API wrapper service in the front-end application. In the service itself we place a .catch for any of the API endpoints that have documented error codes (we used Swagger to document and define our API). In that .catch we handle those error codes and return a proper error. When our controllers/directives consume the service they get back a much stricter set of data. If an error occurs then the UI is usually safe to just display the error message sent from the wrapper service and won't have to worry about looking at error codes.
Likewise for successful responses we do much of what you're doing in option 2. In many cases we refine the data down to what is minimally useful in the actual app. In this way we keep a lot of the data churning and formatting in the service and the rest of the app has a lot less to do. For instance, if we need to create an object based on that data we'll just do that in return the object to the promise chain so that controllers aren't doing that all over the place.
I would choose option two, as it your options are really mostly the same. But let see when we add a model structure like a Person suppose.
comparison.factory('Info', ['$http', function($http) {
var retrievalFile = 'retrievalFile.json';
return {
retrieveInfo: function() {
return $http.get(retrievalFile).then(function(response) {
//we will return a Person...
var data = response.data;
return new Person(data.name, data.age, data.gender);
});
}
}
}]);
This is really simple, but if you have to map more complex data into object models (you retrieve a list of people with their own items... etc), that's when things get more complicated, you will probably want to add a service to handle the mapping between data and models. Well you have another service DataMapper(example), if you choose your first option you will have to inject DataMapper into your controller and you will have to make your request through your factory, and map the response with the injected service. And then you probably say, Should I have all this code here? ... Well probably no.
That is an hypothetical case, something that count a lot is how you feel structuring your code, won't architecture it in a way you won't understand. And at the end take a look at this: https://en.wikipedia.org/wiki/SOLID_(object-oriented_design) and research more information about this principles but focused to javascript.
Good question. A couple of points:
Controllers should be view centric versus data centric therefore you
want remove data logic from the controller and rather have it focus
on business logic.
Models (M in MVC) are a data representation of your application and
will house the data logic. In Angular case this would be a service
or factory class as you rightfully pointed out. Why is that well for
example:
2.1 AccountsController (might have multiple data models injected)
2.1.1 UserModel
2.1.2 AuthModel
2.1.3 SubscriptionModel
2.1.4 SettingsModel
There are numerous ways to approach the data model approach, but I would say your service class should be the data REST model i.e. getting, storing, caching, validating, etc. I've included a basic example, but suggest you investigate JavaScript OOP as that will help point you in the right direction as to how to build data models, collections, etc.
Below is an example of service class to manage your data.Note I have not tested this code but it should give you a start.
EXAMPLE:
(function () {
'use strict';
ArticleController.$inject = ['$scope', 'Article'];
function ArticleController($scope, Article) {
var vm = this,
getArticles = function () {
return Article.getArticles()
.then(function (result) {
if (result) {
return vm.articles = result;
}
});
};
vm.getArticles = getArticles;
vm.articles = {};
// OR replace vm.articles with $scope if you prefer e.g.
$scope.articles = {};
$scope.userNgClickToInit = function () {
vm.getArticles();
};
// OR an init on document ready
// BUT to honest I would put all init logic in service class so all in calling is init in ctrl and model does the rest
function initArticles() {
vm.getArticles();
// OR chain
vm.getArticles()
.then(getCategories); // doesn't here, just an example
}
initArticles();
}
ArticleModel.$inject = ['$scope', '$http', '$q'];
function ArticleModel($scope, $http, $q) {
var model = this,
URLS = {
FETCH: 'data/articles.json'
},
articles;
function extract(result) {
return result.data;
}
function cacheArticles(result) {
articles = extract(result);
return articles;
}
function findArticle(id) {
return _.find(articles, function (article) {
return article.id === parseInt(id, 10);
})
}
model.getArticles = function () {
return (articles) ? $q.when(articles) : $http.get(URLS.FETCH).then(cacheArticles);
};
model.getArticleById = function (id) {
var deferred = $q.defer();
if (articles) {
deferred.resolve(findArticle(id))
} else {
model.getBookmarks().then(function () {
deferred.resolve(findArticle(id))
})
}
return deferred.promise;
};
model.createArticle = function (article) {
article.id = articles.length;
articles.push(article);
};
model.updateArticle = function (bookmark) {
var index = _.findIndex(articles, function (a) {
return a.id == article.id
});
articles[index] = article;
};
model.deleteArticle = function (article) {
_.remove(articles, function (a) {
return a.id == article.id;
});
};
}
angular.module('app.article.model', [])
.controller('ArticleController', ArticleController)
.service('Article', ArticleModel);
})()
Following is my myService.spec.js :
'use strict';
describe('myService', function () {
var dependentService,dependentService1,rootScope,$q;
beforeEach(module('myModule.myConfig'));
beforeEach(module('myModule'));
beforeEach(inject(function (_myService_, _$rootScope_,
_$q_,_dependentService1_,_dependentService_) {
myService= _myService_;
rootScope = _$rootScope_.$new();
$q = _$q_;
dependentService1= _dependentService1_;
dependentService= _dependentService_;
spyOn(dependentService1,'setPath');
spyOn(dependentService,'get');
spyOn($q,'all').and.callFake(function(){
var deferred = _$q_.defer();
if($q.all.calls.count() === 1){
deferred.resolve([{path:'./abcd'}]);
}else if($q.all.calls.count() === 2){
deferred.resolve([{path:'./abcd','user': {'userId':'xyz',
'groups':['G1']}}]);
}else{
deferred.resolve({});
}
return deferred.promise;
});
}));
it('should load path, information',function(){
var promise = myService.load();
rootScope.$apply();
expect(dependentService.get).toHaveBeenCalled();
expect(dependentService1.setPath).toHaveBeenCalledWith('./abcd');
});
});
And here is my MyService.js
'use strict';
function myService($q,dependentService1,dependentService){
var appConfigLoaded = false;
function _loadPath(){
return dependentService.get(dependentService1.url);
}
return {
load : function(){
var loadPath = _loadPath(),
finalDeferred = $q.defer();
$q.all([loadPath ]).then(function (results) {
var path = results[0].path ,
user = results[0].user;
dependentService1.setPath(path);
$q.all([_loadDataPromise1(),_loadDataPromise2()]).then(function(results){
finalDeferred.resolve(results);
},function(reason){
finalDeferred.reject(reason);
});
},function(reason){
finalDeferred.reject(reason);
});
return finalDeferred.promise;
}
};
}
angular.module('myModule.myConfig').service('myService', MyService);
Following functions and service which holds them are omitted for brevity but they return promise. I have spied them as well just like I have spied other two services.
loadDataPromise1() and loadDataPromise1()
Now I get some error like Unexpected request GET with URL which points to some headers.template.html. But I am not even making any call to http to get such template nor any of the functions do. which ever call $http.get I have spied them.
I tried with
$httpBackend.flush();
but the same error occur. May be I am doing something basic thing in a wrong way.
If I remove $rootScope.apply() then error goes away. However .then() function in my service attached to first call to $q.all() is not called.
Any pointers to help me out?
Do you have a default route with a templateURL in your application routing? You are probably running into this issue:
https://github.com/angular/angular.js/issues/2717
The workaround (which is annoying) is to put an expectGET for the templateURL in your beforeEach and then flush it.
$httpBackend.expectGET('path/to/template/defaulttemplate.html').respond(200, '');
$httpBackend.flush();
You can do it anywhere in there - I try and keep it at the top or bottom so it is obvious that is a workaround and not part of the testing code. Note you will have to put those lines in EVERY test file since the routing is part of app (not the module under test).
I've got a service whose primary responsibility is redirect to our login page, which is separate since we're doing login as a service.
function redirectToMembership() {
var returnURL = $location.host();
returnURL+="/#/Authorization";
$window.location.href=EnvironmentConfigConstants.membershipURL + "?ReturnURL="+returnURL;
}
So what it does is it gets the current host and appends /#/Authorization to the end and then redirects to the membership URL with that as the returnURL.
Authorization is a state that will handle the session token and userID which are returned from membership.
My question is: How am I supposed to unit test this?
I have a hunch that I need to spyOn $location.href and verify that it is called with the current location, but whenever I fire membershipService.redirectToMembership, phantomJS crashes.
So far this is what I have for tests:
describe('redirectToMembership', function () {
beforeEach(function () {
spyOn($location,'host').andReturn('localhost');
spyOn($location,'port').andReturn(80);
spyOn($window.location,'href');
});
it('should redirect to Membership (duh)', function () {
var success = false;
membershipService.redirectToMembership();
expect($window.location.href).toBe('this');
$rootScope.$digest();
}) ;
});
However, this is giving me two errors:
'undefined' is not a function (evaluating spyOn($location,'host').andReturn('localhost')')
and 'undefined' is not an object (evaluating 'returnURL.indexOf')
Am I doing something wrong?
I am not going to focus on why phantomjs crashes, but the idea here is that mock your dependencies instead of actual instances, i.e mock $location and $window and have then return value as your test expectation. How the redirection happens, how host is returned etc.. should not be a concern for your service so just mock them out.
Something like this:
describe('redirectToMembership', function () {
var $location,
$window = {location:{href:""}},
host="localhost",
port="80",
membershipService,
$rootScope;
beforeEach(function () {
//Set up mocks
module('myModule',function($provide){
//create a spy object with 2 functions
$location = jasmine.createSpyObj('location',['host', 'port']);
$location.host.and.returnValue(host);
$location.port.and.returnValue(port);
//Set the mock object created above
$provide.value('$location', $location) ;
//Set the mock object
$provide.value('$window', $window) ;
});
//get instances
inject(function(_membershipService_, _$rootScope_){
membershipService = _membershipService_;
$rootScope = _$rootScope_;
});
});
it('should redirect to Membership (duh)', function () {
var success = false;
membershipService.redirectToMembership();
expect($window.location.href).toBe(ExpectedContructedURL);
$rootScope.$digest(); //You may not need this
});
});