A controller makes 2 calls to a remote http location to get data.
When data comes a procedure is called. When both requests return data, then data merging is done and some aggregation is performed.
The purpose of a unit test would be to test if the controller works as expected no matter the order of responses.
it("downloads all data and combines it", function() {
...
$httpBackend.expectGET(responsePerDomainQuery).respond(
{ result: [ { result: 2 }, { result: 3 } ] });
$httpBackend.expectGET(responsePerTrQuery).respond(
{ result: [{ result: 1 }, { result: 4 }] });
$controller("Ctrl", { '$scope': $scope });
$httpBackend.flush();
... some expectations ...
}
The test passes but it does not guarantee that any order of successfully responding requests will not break the controller's logic. How can this be achieved?
When I said "no need to test this case" i was referring to the fact that using $q.all already guarantees that the callback is executed only when all of the requests are satisfied. That being said I agree that preparing tests for your own implementation is a good practice, so here's I would do it.
(Mind that this is just pseudo code, some things may need to be tweaked in order to work properly, but that's just to explain how i would tackle this one.)
First of all I would move my AJAX calls away from my controller and provide a dedicated service for them (maybe you already did it this way, if so that's great, bear with me for now).
As an example:
angular.service('myQueries', function($http){
this.myReq1 = function(){
return $http.get(API.url1);
};
this.myReq1 = function(){
return $http.get(API.url2);
};
});
Then I would test this service on its own normally using $httpBackend.expectGET().
I would then get back to the controller and use that service in there as specified in my comments to the question:
angular.controller('myCtrl', function($scope, myQueries, $q){
// at load time query for results
$q.all([myQueries.myReq1(), myQueries.myReq2()])
// everything after this is guaranteed to be run ONLY when
// both responses are in our hands
.then(doSomethingWithBoth)
// one or both requests went bad
// let's handle this situation too.
.catch(someThingWentBad);
function doSomethingWithBoth(data){
$scope.myData = data;
}
function someThingWentBad(data){
$scope.disaster = true;
}
});
At this point we can test our controller and inject a mocked service into it. Many ways to do it but something similar should do:
var scope, controller, fakeService, q, dfd1, dfd2;
beforeEach(function(){
fakeService = {
myReq1: function(){
dfd1 = q.defer();
return dfd1.promise;
},
myReq2: function(){
dfd2 = q.defer();
return dfd2.promise;
},
};
})
beforeEach(inject(function ($rootScope, $controller, $q) {
q = $q;
scope = $rootScope.$new();
controller = $controller('myCtrl', { $scope: scope, myQueries: fakeService });
}));
At this point you are free to resolve/reject the promises exactly when you want. You can check what happens when the first response is faster than the second:
it('should do this when one response is faster', function(){
dfd1.resolve('blabla');
// myReq2 is still pending so doSomethingWithBoth() has not yet been called
scope.$apply();
expect(scope.myData).toBe(undefined);
dfd2.resolve('i am late, sorry');
scope.$apply();
expect(scope.myData).not.toBe(undefined);
});
You can check what happens when the second response is faster than the first:
it('should do this when the other response is faster', function(){
dfd2.resolve('here is a response');
// myReq1 is still pending so doSomethingWithBoth() has not yet been called
scope.$apply();
expect(scope.myData).toBe(undefined);
dfd1.resolve('i am late, sorry');
scope.$apply();
expect(scope.myData).not.toBe(undefined);
});
Or what happens when one of those fails:
it('should do this when one response fails', function(){
dfd1.resolve('blabla');
dfd2.reject();
scope.$apply();
expect(scope.disaster).toBeTruthy();
});
We can use something like where alpha var will have response from 1st call n so on....
var promiseAlpha= $http({method: 'GET', url: 'a/pi-one-url', cache: 'true'});
var promiseBeta= $http({method: 'GET', url: '/api-two-url', cache: 'true'});
let promises = {
alpha: promiseAlpha,
beta: promiseBeta
}
$q.all(promises).then((values) => {
console.log(values.alpha); // value alpha
console.log(values.beta); // value beta
console.log(values.gamma); // value gamma
complete();
});
Related
I'm trying to get a factory JSON response, save it in a variable, in order to be ready to be called from 2 different controllers.
Here bellow I paste the code I'm using:
storyFactory.js
var story = angular.module('story.services', []);
story.factory('storyAudio', [ '$http', function ($http) {
var json = {};
function getJSON(story_id, callback) {
$http({
url: 'https://api.domain.co/get/' + story_id,
method: "GET"
}).success(function (data) {
json = data;
callback(data);
});
};
return {
getSubaudios: function(story_id, callback) {
getJSON(story_id, function(result) {
callback(result);
});
},
getTopbar: function(callback) {
callback(json);
}
};
}]);
StoryCtrl.js
var storyCtrl = angular.module('story', ['story.services']);
storyCtrl.controller('storyCtrl', [ 'CONFIG', '$stateParams', 'storyAudio', function(CONFIG, $stateParams, storyAudio) {
var data = this;
data.story = {};
storyAudio.getSubvideos($stateParams.story_id, function(response) {
data.story = response;
});
}]);
TopbarCtrl.js
var topbarCtrl = angular.module('topbar', ['story.services']);
topbarCtrl.controller('topbarCtrl', [ 'CONFIG', '$stateParams', 'storyAudio', function(CONFIG, $stateParams, storyAudio) {
var data2 = this;
data2.story = {};
storyAudio.getTopbar(function(response) {
data2.story = response;
});
}]);
The problem is in my TopbarCtrl response I'm receiving an empty data2.story when I call it in the HTML.
The reason is because it doesn't have a callback of the $http response, so it prints the var json with the actual status, that is an empty object.
How could I load the second controller when the variable has content?
Thanks in advice.
I think the best you can do in this case is load the data via getSubaudios and provide a reference to the data for other controllers to use. Something like this...
story.factory('storyAudio', function($http) {
var factory = {
story: {}
};
factory.getSubaudios = function(story_id) {
return $http.get('https://api.domain.co/get/' + story_id).then(function(response) {
return angular.extend(factory.story, response.data);
});
};
return factory;
})
Using angular.extend() instead of directly assigning a value to the factory's story property maintains any references that may be established before the data is loaded.
Then you can load the data via
storyCtrl.controller('storyCtrl', function(storyAudio) {
var data = this;
storyAudio.getSubaudios($stateParams.story_id).then(function(story) {
data.story = story;
});
})
and directly reference the story data by reference in your controller
topbarCtrl.controller('topbarCtrl', function(storyAudio) {
this.story = storyAudio.story;
})
I think I'm understanding correctly, but let me know if not.
There are two issues I'm seeing. The first is that there is a typo in your StoryCtrl.js file. You are calling "storyAudio.getSubvideos" but the function is called "getSubaudios" in your factory.
Even with that typo fixed, the issue could still technically happen. It all really depends on how quickly the promise returns from the first call. Unfortunately, promises are asynchronous, so there is no guarantee that the "json" variable will get set before the second controller tries to get it.
In order to resolve this, you need to ensure that the first call is finished before trying to access the "json" variable you have on the service. There are probably a few different ways to do this, but one that comes to mind is to actually return and store the promise in the service like so...
var dataPromise;
function getSubaudios(story_id){
if(!dataPromise){
dataPromise = $http({
url: 'https://api.domain.co/get/' + story_id,
method: "GET"
});
}
return dataPromise;
}
return {
getSubaudios: getSubAudios
};
Then in your controllers, you can just call the service and use .then to get the data out of the promise when it returns...
storyAudio.getSubaudios($stateParams.story_id).then(function(response){
data.story = response; //or data2.story = response;
});
Here is a plunkr example. I've used the $q library to simulate a promise being returned from an $http request, but it should illustrate the idea.
Similar to Phil's answer. (Angular extend, or angular copy keeps the references the same in both controllers. If you don't want to put watchers in both controllers to keep track if the value changes.) Several methods here:
Share data between AngularJS controllers.
You could also bind the object you are returning directly to the update-function. That way the references stay intact.
storyServices.factory('storyAudio', ['$http', function($http) {
return {
data: { json: '' },
getSubaudios: function(story_id) {
$http.get('http://jsonplaceholder.typicode.com/posts/' + story_id)
.then(function(response) {
this.data.json = response.data.body;
}.bind(this));
}
};
}]);
var storyCtrl = angular.module('story').controller('storyCtrl', ['$scope', 'storyAudio', function($scope, storyAudio) {
$scope.data = storyAudio.data;
storyAudio.getSubaudios(2);
}]);
var topbarCtrl = angular.module('story').controller('topbarCtrl', ['$scope', 'storyAudio', function($scope, storyAudio) {
$scope.data2 = storyAudio.data;
}]);
Plunk here: http://plnkr.co/edit/auTd6bmPBRCVwI3IwKQJ?p=preview
I added some scopes to show what happens.
Sidenote:
I think it's straight out dishonest to name your non-controller "storyCtrl" and then assign it a controller of its own:
var storyCtrl = angular.module(...); // Nooo, this is not a controller.
storyCtrl.controller(...); // This is a controller! Aaaah!
Another sidenote:
.success() is the old way of doing things. Change to .then(successCallback) today! I dare to say it's the standard convention for promises.
https://docs.angularjs.org/api/ng/service/$http#deprecation-notice
I try to get some important things like: companyid,employeeid etc. with every request that a user makes. So this has to be received before everything else is done.
After that the user receives information based on his companyid that he sets with every request (get/company/{companyid}).
The problem that I have is that the response for receiving the companyid takes to long and angular already tries to make a request to (get/company/{companyid}) obviously there is no companyid yet.
I've tried to fix this whit promise but it's not working.
Here I try to receive some important information about the user(that I do with every request) :
Service
(function () {
angular.module('employeeApp')
.service('authenticationservice', authenticationservice);
function authenticationservice($http,$location,authenticationFactory,$q,GLOBALS,$cookies) {
this.validateUser = function () {
var vm = this;
vm.deferred = $q.defer();
data = {"api_token": api_token};
return $http.post(GLOBALS.url+'show/employee/' + $cookies.get('employeeid'),data)
.success(function(response)
{
vm.deferred.resolve(response);
})
.error(function(err,response)
{
vm.deferred.reject(err);
});
return vm.deferred.promise;
}
}
})();
Routes file
(In my routes file I use the authenticationservice to set all important users variables.)
employeeAppModule.run([
'authenticationservice',
'constants',
function(authenticationservice,constants) {
authenticationservice.validateUser()
.then(function(response)
{
constants.companyid = response.result.Employee;
constants.role = response.result.Role;
constants.name = response.result.FirstName;
console.log('test');
},
function(response){
console.log('error');
});
}
]);
So the problem is that the user information is set to late and angular already goes to my homeController where he uses the companyId that is not being set yet.
Thankyou
The problem in your current code is return $http.post are having two return statement in your validateUser method. Which is returning $http.get before returning return vm.deferred.promise; & that why customly created promise doesn't get returned from your method. Though by removing first return from $http.get will fix your problem, I'd not suggest to go for such fix, because it is considered as bad pattern to implement.
Rather I'd say, you should utilize promise return by $http method, & use .then to return data to chain promise mechanism.
Code
function authenticationservice($http, $location, authenticationFactory, $q, GLOBALS, $cookies) {
this.validateUser = function() {
var vm = this;
data = {
"api_token": api_token
};
return $http.post(GLOBALS.url + 'show/employee/' + $cookies.get('employeeid'), data)
.then(function(response) {
var data = response.data;
retrun data;
}, function(err) {
return $q.reject(err);
});
}
}
To make sure that $ http return a $ promise object you need to check that the action in the controller returns a value and it is not a void action.
BACKGROUND
As my app grows I’m struggling more and more with AngularJS promise synchronization / sequencing across multiple controllers and services. In my example below I have an articles controller ArticleController and related service ArticleDataService that in the initial load process
gets / loads articles from a server,
selects the first article from articles and
uses this current article currentArticle to get the related images from the server.
THE PROBLEM
Data load from the server takes approx. 1 second to return the articles records, then as above select first article and then also return the related image records from the server. The problem is that during that latency period the second controller (ImagesController) is looking for the cached data in the second Services module (ImageDataService) and cannot find it because the first promises have obviously not resolved from the Article Controller due to server latency and as such the ArticleController hasn't cached the images data yet, which then blows up any following image related code. As you can see below, if I try to return a $q.when(cachedImages) on cachedImages, it will returns a promise, but that promise is never resolved. As both controllers are using separate $q resolve sequences it makes sense, but without building an uber controller I'm unsure how to fix the sequencing issues.
EDIT
I'm trying to solve for the n:n chaining / sequencing problem
Most tutorials or discussions tend to focus on 1:1 or 1:n chaining which works perfectly. No problem there.
It is the n:n where I'm having problems i.e. ctrl to service, service to service and n ctrl to service. Most of what I can find on n:n are basic tuts loading simple static arrays or object which doesn't have the latency issue.
ATTEMPTED APPROACHES
rootscope / watch : I've tried $rootscope.$watch() events inside of services as well as watch in controllers i.e. an event based approach on the imageCached var inside of the ImageDataService, but frankly I find that messy as there can be unnecessary overhead, debugging and testing issues.That said, it does work but every now and then I will see lots of iteration when console logging a deeply nested var which makes the event approach seem black boxish.
EDIT - watch approach
Example: I can add the following to the 2nd controller ImageController or in the ImageDataService which works, as would $emit, and then kill the watcher, but as I said this does require a bit of time management for dependent methods such as chart data directives. Also, I wondering if mixing promises and events is bad practice or is that the accepted best practice in JS?
var articleModelListener = $scope.$watch(function () {
return ImageDataService.getImages();
},
function (newValue, oldValue) {
if (newValue !== undefined && newValue !== null) {
if (Object.keys(newValue).length > 0) {
iCtrl.dataUrls = newValue;
// kill $watcher
articleModelListener();
}
}
});
timeout : I've also tried to wrap all ImageController code in timeout AND also document ready, but I find that has further repercussions down the line e.g. in my Chart and Poll controllers I have to wrap directives in additional $timeouts or $intervals to adjust for the ImagesController time intervals or the directives won't load the DOM attributes correctly, so it becomes a chain of app performance death.
uber DataService service or factory data resolution : I've tried to resolve all data in an uber DataServices service provider but I find I have the same issue now in all controllers as although the uber service fixes the sequencing I now need to get synchronization with uber and all controllers. I know async ... give me state programming any day :)
QUESTION & ASSUMPTION
Assumption: timeout and interval wrapping are bad practices / anti-pattern as that is waterfall?
Is the best approach to stick with promises and if so is there a way to get promises to synchronize or said better sequentially resolve across multiple controllers / services OR do I keep going down the events approach using rotoscope watches and controller scope watches?
Example of my code / problem below:
PLEASE NOTE:
1. I've removed code for brevity sake i.e. I have not tested this summary code, but rather using it to example the problem above.
2. All and any help is much appreciated. Apologies for any terminology I've misused.
HTML
<section ng-controller="MainController as mCtrl">
// removed HTML for brevity sakes
</section>
<section ng-controller="ImagesController as iCtrl">
// removed HTML for brevity sakes
</section>
Angular JS (1.4.*)
<pre><code>
angular.module('articles', [
])
.controller('ArticlesController', ['ArticleDataServices', 'ImageDataService', function(ArticleDataServices, ImageDataService) {
var mCtrl = this;
mCtrl.articles = {};
mCtrl.currentArticle = {};
mCtrl.images = {};
var loadArticles = function () {
return ArticleDataServices
.getArticles()
.then(function (articles) {
if(articles.data) {
mCtrl.articles = articles.data;
return mCtrl.articles[Object.keys(mCtrl.articles)[0]];
}
});
},
loadCurrentArticleImages = function (currentArticle) {
return ImageDataService
.getArticleImages(currentChannel)
.then(function (imagesOfArticles) {
if(imagesOfArticles.data) {
return mCtrl.images = imagesOfArticles.data;
}
});
},
cacheImages = function (images) {
return ImageDataService
.parseImages(images)
.then(function () {
});
};
loadChannels()
.then(loadArticles)
.then(loadCurrentArticleImages)
.then(cacheImages);
}])
</code></pre>
NOTE : it is in the ImagesController below where things go wrong as this controller is executing its methods ahead of the first controller which is still waiting on data from server i.e. cachedImages or promise is not returning.
<pre><code>
.controller('ImagesController', ['ImageDataService', function(ImageDataService) {
var iCtrl = this;
mCtrl.images = {};
var getCachedImageData = function () {
return ImageDataService
.getCachedImageData()
.then(function (images) {
if(images) {
return mCtrl.images = images;
}
});
}
}])
.service('ArticleDataServices', ['$http',', $q', function($http, $q){
var model = this,
URLS = {
ARTICLES: 'http://localhost:8888/articles'
},
config = {
params: {
'callback': 'JSON_CALLBACK',
'method': 'GET',
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
}
};
model.getArticles = function() {
config.params['url'] = URLS.ARTICLES;
return $http(config.params);
};
return {
getArticles: function () {
var deffered = $q.defer();
deffered.resolve(model.getArticles());
return deffered.promise;
}
}
}])
.service('ImageDataService',['$http',', $q', function($http, $q){
var model = this,
URLS = {
IMAGES: 'http://localhost:8888/images'
},
cachedImages,
config = {
params: {
'callback': 'JSON_CALLBACK',
'method': 'GET',
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
}
};
model.getArticleImages = function(currentArticle) {
config.params['url'] = URLS.IMAGES + '/' . currentArticle.slug;
return $http(config.params);
};
// Return images or $q.when
model.getCachedImageData = function() {
if (cachedImages) {
return cachedImages
} else {
return $q.when(cachedImages);
}
};
model.setImageCache = function(images) {
cachedImages = images;
};
return {
getArticleImages: function (currentArticle) {
var deffered = $q.defer();
deffered.resolve(model.getArticleImages(currentArticle));
return deffered.promise;
},
setImageCache:function (images) {
return model.setImageCache(images);
},
getCachedImageData:function () {
return getCachedImageData();
}
};
}]);
</code></pre>
Your problem is common for people when initializing with angular. The correct is return promise in your service as:
app.controller("AppController", function($scope, $ajax){
$ajax.call("/people", "", "POST").then(function(req) {
$scope.people = req.data;
});
});
app.factory("$ajax", function($http) {
function ajax(url, param, method) {
var requisicao = $http({
method: method,
url: url,
data:param
});
var promise = requisicao.then(
function(resposta) {
return(resposta.data);
}
);
return promise;
}
return({
call:ajax
});
});
Note that the variable is populated only in the return of service. It is important you put all methods or anything else that makes use of such a variable within Then method. This will ensure that these other methods will only be executed after returning from the backend
I am new to Angular, and am trying to get up to speed with ngResource.
I created a factory in my chapter.service.js file
angular.module('myApp')
.factory('Chapter', function ($resource) {
return $resource('/api/book/chapter/:id'); // Note the full endpoint address
});
matchescontroller.js
angular.module('myApp').controller('matchesCtrl', function($scope, $location, Chapter) {
// This is used to get URL parameters
$scope.url = $location.path();
$scope.paths = $scope.url.split('/');
$scope.id = $scope.paths[2];
$scope.action = $scope.paths[3];
//Trying to call the test data
var chapters = Chapter.query();
$scope.myFunction = function() {
alert(chapters.length);
}
My view where I test the function
<button ng-click="myFunction()">Click Here</button>
I created a test function to test whether my query returned any results. When I click on the button, I'm alerted with 0, which means the query didn't work.
When I change the function to
$scope.myFunction = function() {
console.log(Object.keys(chapters));
}
I get [$promise, $resolve], but none of the Schema keys
I must be doing something wrong, but I was looking at this tutorial
http://www.masnun.com/2013/08/28/rest-access-in-angularjs-using-ngresource.html
Any help will be appreciated.
Edit: Here is the response I got from the server
GET http://localhost:9000/api/book/chapter/1 500 (Internal Server Error)
$scope.myFunction = function() {
Chapter.query({}, function(data) {
$scope.chapters = data;
}, function(error) {
// custom error code
});
}
When working with $resource I prefer to use the success/error handlers that the API comes with as opposed to dealing the promise directly. The important thing to realize is that just because you called query does not mean that the result is immediately available. Thus the use of a callback that handles success/error depending on what your backend returns. Only then can you bind and update the reuslt in the UI.
Also, while we're talking about it I notice that you didn't wire up the optional paramter in your $resouce URL. $resource takes a second paramter which is an object that supplies mapping for the /:id part of your route.
return $resource('/api/book/chapter/:id', {id: '#id'});
What this notation means is that you pass an object to $resource that has a property called id, it will be subbed into your URL.
So this:
$scope.item = {id: 42, someProp: "something"};
Chapter.get({$scope.item}....
Will result in an API call that looks like '/api/book/chapter/42'
You get a promise from $resource and not the "result" from your database. The result is inside the promise. So try this
var chapters = Chapter.query();
$scope.myFunction = function() {
chapters.then(function(data) {
console.log(data);
});
}
I must admit, that I am not thaaaaat familiar with ngResource, so Jessie Carters is right, the correct syntax is:
chapters.get({...}, function callback() {...})
After too many hours, I cannot for the life of me get this example working. I'm following Ben Lesh's excellent guides to mocking $http requests in Angular, but for some reason the service request is not sending.
I have verified that the service is working properly by building a separate HTML DOM and applying my app and a tiny controller to it. I'm using Jasmine 1.3 and Angular 1.2.9 (and angular-mocks.js of course).
Here's the app:
var app = angular.module('myApp', []);
app.factory('httpBasedService', function($http) {
return {
sendMessage: function(msg) {
return $http.get('something.json?msg=' + msg)
.then(function(result) {
console.log(result.data)
return result.data;
});
}
};
});
And the test:
describe("httpBasedService ", function () {
var httpBasedService,
mockBackend;
beforeEach(function (){
module('myApp');
inject(function(_$httpBackend_, _httpBasedService_) {
mockBackend = _$httpBackend_;
httpBasedService = _httpBasedService_;
});
});
afterEach(function() {
mockBackend.verifyNoOutstandingExpectation();
mockBackend.verifyNoOutstandingRequest();
});
it('should send the msg and return the response.', function () {
var returnData = {excited: true};
mockBackend.expectGET('something.json?msg=wee').respond(returnData);
var returnedPromise = httpBasedService.sendMessage('wee');
var result;
returnedPromise.then(function(response) {
result = response;
});
mockBackend.flush();
expect(result).toEqual(returnData);
});
});
I'm getting a Error: No pending request to flush ! and of course a Error: Unsatisfied requests: GET (since nothing has been sent). I watch the network requests and sure enough, no request is sent, even if I remove the mock backend.
I've commented out stuff, restructured stuff, tried lots of other examples, but to no avail. Can someone help me?
As usual, when I talk to the duck, the problem is fixed.
Turns out I was using angular-mocks.js from v1.1.0. My angular.js version was 1.2.9.
Facepalm.
Check your versions, future readers, and save yourself a few hours.