In my AngularJS Controller, I have a $scope.init method that gets called when the controller gets initialized. $scope.init calls $scope.loadData and $scope.loadNames, which each fire an HTTP GET Request to an API to fetch some data.
I have written some unit tests using $httpBackend to mock the API responses and test $scope.loadData and $scope.loadNames. Those work fine but something that I can't seem to test is the $scope.init method. Is there a way to mock or stub a method that gets called on controller initialization? What are the best practices for this?
You can verify that the scope objects changed in $scope.init have the proper values after the controller initialization. You can even try to change the values of those objects, and rerun the init method, and check that everything matches afterwards
You can test:
the state of the controller is as expected after it is "initialized"
verify that loadnames and loaddata gets called (jasmine's spyOn+toHaveBeenCalled)
play with a promise object mocking the loadnames/loaddata methods response to verify your behavior under unexpected conditions
Related
I am reading
http://angular-tips.com/blog/2013/08/watch-how-the-apply-runs-a-digest/
directive link function:
element.bind('click', function() {
scope.foo++;
scope.bar++;
scope.$apply();
});
a better way for using $apply:
element.bind('click', function() {
scope.$apply(function(){
scope.foo++;
scope.bar++;
});
});
What’s the difference? The difference is that in the first version, we are updating the values outside the angular context so if that throws an error, Angular will never know. Obviously in this tiny toy example it won’t make much difference, but imagine that we have an alert box to show errors to our users and we have a 3rd party library that does a network call and it fails. If we don’t wrap it inside an $apply, Angular will never know about the failure and the alert box won’t be there.
Confusion:
Why angular need to know error, i just need to show it for users. for example, there is an ajax request in link fn of directive, I just need to tell what happened if fails.
TAngular $scope has a function called $apply() which takes a function as an argument. AngularJS says that it will know about model mutation only if that mutation is done inside $apply(). So you simply need to put the code that changes models inside a function and call $scope.apply(), passing that function as an argument. After the $apply() function call ends, AngularJS knows that some model changes might have occurred. It then starts a digest cycle by calling another function —- $rootScope.$digest() — which propagates to all child scopes. In the digest cycle watchers are called to check if the model value has changed. if a value has changed, the corresponding listener function then gets called. Now it’s upto the listener how it handles the model changes.
The Ajax call through Angular buildin $http the model mutation code is implicitly wrapped withing $apply() call, so you don’t need any additional steps.
Method on $viewcontentloaded is firing asynchronously. To detail my problem, I have a variable in root scope i.e. my Main controller, which need to be initialized before my view controller loads. In module.run I am calling a sync function to initialize $rootScope.session. And In my view controller of a route, I am checking the status of session in afunction that is called like
$scope.$on('$viewContentLoaded', function() {
$scope.initialize();
});
But some times on page refreash, I am getting an undefined value for $rootScope.session, as It may have initialized later. So, Is there any way to make this synchronous like rootscope will be initialized before view loads. And for curiosity, how it will affect, if I call the $scope.initialize(); normally in my controller, in $viewContentLoaded or in $routeChangeSuccess.
Thanks in advance.
So, Is there any way to make this synchronous like rootscope will be initialized before view loads.
Use the $controller service to manually create the controller, as in a unit test.
$controllerProvider.register('FooCtrl', FooCtrl);
ctrl = $controller('FooCtrl', {$scope: scope});
Or $broadcast a custom event from the main controller down to the child:
function mainCtrl($rootScope)
{
$rootScope.$broadcast('abc');
}
function secondCtrl($scope)
{
$scope.$on('abc', function(event) { $scope.initialize(); });
}
Or use a try/catch block and a recursive call with a timer.
These are more or less the steps that you would take to implement lazy loading in AngularJS. In summary, you would first define your app module to keep instances of the relevant providers. Then you would define your lazy artifacts to register themselves using the providers rather than the module API. Then using a ‘resolve’ function that returns a promise in your route definition, you would load all lazy artifacts and resolve the promise once they have been loaded. This ensures that all lazy artifacts will be available before the relevant route is rendered. Also, don’t forget to resolve the promise inside $rootScope.$apply, if the resolution will be happening outside of AngularJS. Then you would create a ‘bootstrap’ script that first loads the app module before bootstrapping the app. Finally, you would link to the bootstrap script from your ‘index.html’ file.
References
AngularJS source: controllerSpec.js
Ifeanyi Isitor: Lazy Loading In AngularJS
AngularJS Lazy Loading with Require.js
Split Large AngularJS Controllers using the Mixin Pattern
I have a resource which wraps a RESTful API. I use that resource from my controller to create new objects and save them, similar to this snippet from the Angular docs:
var newCard = new CreditCard({number:'0123'});
newCard.name = "Mike Smith";
newCard.$save();
When writing a unit test in Jasmine, I get the following error when the $save call is executed: "Cannot read property '$promise' of undefined".
What's the best approach to testing the method in my controller which contains the above code?
If you use Jasmine's spyOn() function to verify that a $resource method is called, it overwrites the original $resource method with one that implements the "spying" functionality.
If the code in your application relies on the $resource setting the $promise property, or it relies on the returned object/array from the $resource, the Jasmine's spy function won't return anything or set a value on the $promise property. As a result, perfectly fine code in your application will fail when being tested. A similar thing happens when you use $http with the then(), success(), or error() functions.
To work around that you can make Jasmine spy on the function as well as call the original function by doing something like this:
// Newer Jasmine 2.0 syntax:
spyOn(resource, "$save").and.callThrough();
// Older syntax:
spyOn(resource, "$save").andCallThrough();
Angular is loosing my my JSON values, and i'm trying to find out why. I think I need to make a promise or a time out, or maybe apply. I'm not quite sure...
angular version: 1.2.1
galleryApp.controller('GalleryCtrl', function ($scope, $http, $routeParams, $filter){
$http.get('mygallery.json').success(function(data) {
$scope.gallery = data;
//DISPLAY: CONTENTS OF JSON IN OBJECT : WORKS
console.log($scope.gallery);
});
//DISPLAY: undefined : DOES NOT WORK AS EXPECTED
console.log($scope.gallery);
//DISPLAY: CONTENTS OF OBJECT: I can see scope.gallery exists!
//I just can't seem to access scope.gallery directly outside of http.get()
console.log($scope);
});
Note: scope.gallery or "gallery" works perfectly fine in my view! {{gallery.name}} etc.
It seems like there is some behind the scenes things angular is doing to the scope, or some concept that i'm not quite grasping.
Well, it's trivial as $http.get is an asynchronous operation. So while it is working the rest of the code will be finished. If you log the $scope.gallery it is undefined yet. If you log the $scope it's still undefined but will be updated when the success callback is invoked. Th reason of this effect for you is just feature of console.log which writes not the current snapshot but the object itself so if changed the output of the console will be updated respectively. But in general none of your code outside of the $http.get will work as you expected here. So you should either use the success callback or use $watch to track the changes.
Also refer to this documentation: $http, $q
The result of http.$get is a promise (specifically an HtmlPromise, see the $http docs). A promise is described as the following in the $q docs:
an interface for interacting with an object that represents the result of an action that is performed asynchronously, and may or may not be finished at any given point in time
The success method of an HtmlPromise takes a callback function to run once the promise is resolved (meaning the action is complete). The anonymous function thus runs asynchronously. When it is logged outside of the callback, the callback has not yet been run and therefore the scope variable has not been set.
I imagine the view is as expected because the http request completes and the view is updated so fast, it is imperceivable.
Say I have an service:
angular.module("app").factory("myService", function($rootScope){
return {
doSomething: function(){
console.log('doingSomething', $rootScope.$$phase);
$rootScope.someVar = true;
}
}
});
If I run it inside the controller like this
angular.module("app").controller("HomeController", function(myService){
myService.doSomething();
});
The console log gives:
doingSomething $apply
But if I run the service inside a unit test environment
it('should doSomething', inject(function(myService) {
myService.doSomething();
}));
The console log only get
doingSomething null
The typical answer that if you need to trigger the $digest cycle yourselves through $apply(). The angular js wiki mentioned about ng-click, $timeout and $http, but surely there are other places, such as running inside the controller. How can I determine that without trial and error?
It really depends on what you are testing. If you are testing something that requires a digest cycle (like $broadcast or $on events), then you will need to call scope.$apply() or simply scope.$digest() in the setup of the test.
However, most of the time, this is not required because services are usually designed as discrete pieces of functionality that do not require the scope. In the case of an HTTP call, the $httpBackend` service allows you to mock out the response.
For ng-click, you shouldn't be using this is a service. And for a controller you'll be binding to a function that you can test in isolation.
Hope this helps.