I don't really understand when I should use the $rootScope = $rootScope.$new() in my angular unit tests. This snipped you see in many unit test examples. But in the following example that doesn't work:
angular.module("app.root", []).factory("rootFct", function ($rootScope) {
$rootScope.amount = 12;
return {
getAmount: function () {
return ($rootScope.amount + 1);
}
}
});
The associated unit Test doesn't work:
describe('Amount Tests', function() {
var rootFct, $rootScope;
beforeEach(function() {
angular.mock.module("app.root");
angular.mock.inject(function (_rootFct_, _$rootScope_) {
$rootScope = _$rootScope_.$new();
rootFct = _rootFct_;
});
});
it('Set Amount in rootScope on 10', function() {
var result = rootFct.getAmount();
expect(result).toBe(13);
$rootScope.amount = 15;
result = rootFct.getAmount();
expect(result).toBe(16);
});
});
it only works when I am changing the
$rootScope = _$rootScope_.$new();
to
$rootScope = _$rootScope_;
and so I don't really understand when to use the $new() and for what its good for?
$new() is mainly used when creating a new scope out of an existing
scope.
Your code is already injecting rootScope in the service so it would not hold any difference.
$new() can be used when you want to get some attributes from the parent scope with/without isolation.
It takes two params isolate and parent.
Isolate (boolean) need to be used if isolation from parent scope is required.
Parent (object) Explicitly defining the parent scope.
Do note that a new scope created manually needs to be destroyed manually as well.
More on it over here
As we know $rootScope is a global scope in out Angular app, and it holds true for unit test too. Angular framework will create a $rootScope object while unit testing too.
Since you are injecting your service into the test, Angular DI uses the global $rootScope and automatically injects it into the service.
You doing $rootScope = _$rootScope_.$new(); will not make any different. Here you are not injecting the dependency into the service, as is the case with controller, where we create scope with $new and inject it using the $controller
You need to have the original $rootScope to assert your service behavior.
Related
I have the below code in one controller.
$scope.DataModel= [];
$scope.DataModelTexts= { buttonDefaultText: '' };
I will be using the same code in one more controller. Now instead of writing the same code in 2nd controller too, i want to know if there is a way to put this in a common code in some factory or service and use that in both the controllers. I have tried to read about factory and services and i am getting a bit confused of how to use any one of these in my scenario.
Any information would help me gain more knowledge about factory and services in angularjs. Thanks.
You're on the right track, use can use a factory or a service to share code between controllers. Note that in angular services(and factories) are singletons; they are instantiated once when the app starts and then anytime you inject it into a controller, you are referencing the same instance. Consider the following code:
var myApp = angular.module('myApp',[]);
myApp.service('MyService', function() {
let _someValue = 'Initial Value';
this.setValue = function(value){
_someValue = value;
}
this.getValue = function(){
return _someValue;
}
});
//First Controller Run
myApp.controller('ControllerA', function($scope, MyService) {
MyService.getValue(); //Initial Value
MyService.setValue("BRAND NEW VALUE!!!");
});
//Run after ControllerA
myApp.controller('ControllerB', function($scope, MyService) {
MyService.getValue(); //BRAND NEW VALUE!!!
});
Her you'll see that MyService holds the state of someValue. ControllerA get MyService injected to it and can use the methods of that service to set a new value. Now for any subsequent call for that same state, like for instance by ControllerB, the updated value will be returned.
You can use the .config() or a run() blocks (good SO on these here: AngularJS app.run() documentation?) to bind these reused variables to $rootScope, then call them from $rootScope.DataModel and $rootScope.DataModelTexts from within your controllers or services (as long as you inject $rootScope into these controllers and services).
I have a global function declared as follows (only necessary bits):
initiateCheckList = {};
$(function() {
initiateCheckList = function() {
...
}
And then I have a function inside an Angular controller that tries to call that function, but I get the error initiateCheckList is not defined when the following function is called:
$scope.updateSuburbs = function () {
$scope.suburbs.getModel($scope.areas.areaId);
initiateCheckList();
};
This function is nested inside a controller, and is bound to a dropdown change event like so:
<select class="form-control" ng-model="areas.areaId" ng-change="updateSuburbs()">
What is Angular doing so that I can't call a global function, and how can I fix things so I can call it?
Below is the ideal way of defining global properties/function in angular:
You've got basically 2 options for "global" variables:
use a $rootScope [http://docs.angularjs.org/api/ng.$rootScope][1]
use a service [http://docs.angularjs.org/guide/services][1]
$rootScope is a parent of all scopes so values exposed there will be visible in all templates and controllers. Using the $rootScope is very easy as you can simply inject it into any controller and change values in this scope. It might be convenient but has all the problems of global variables.
Services are singletons that you can inject to any controller and expose their values in a controller's scope. Services, being singletons are still 'global' but you've got far better control over where those are used and exposed.
Using services is a bit more complex, but not that much, here is an example:
var myApp = angular.module('myApp',[]);
myApp.factory('UserService', function() {
return {
name : 'anonymous',
printData: function() {console.log('Print Data');}
};
});
and then in a controller:
function MyCtrl($scope, UserService) {
$scope.name = UserService.name;
UserService.printData();
}
I hope it would help you to understand adding global properties/functions in angular.
I have been looking around online, but seems, no one really injecting the actual dependencies into a unit test for Angularjs using Jasmine and Karma.
I think there is definitely a separation of concern for the testing process, but I also would like to know how it integrated well with current dependencies in use... so just in case a dependencies is not working well with my component, I will be aware of it!
So, I wonder how can I inject the actual dependencies? So far I found online articles are all about mocking it with a fake one... But I want to use the actual one. Right now, when I enter karma start I am getting a error of Error: [$injector:unpr] Unknown provider: _Provider <- _ <-MyService
I inject services in forEach block like this
beforeEach(angular.mock.inject(function(_MyService_) {
I wonder if its because I am not using the fake service?
describe('MyCtrl', function() {
//Data Exposure Prep
var $controller;
var $rootScope;
var $scope;
var controller;
var MyService;
dd1 = {
itinerary: globalMockData.d1,//I stored globalMockData somewhere else
};
beforeEach(angular.mock.module('myapp'));
beforeEach(angular.mock.inject(function(_$rootScope_, _$controller_, _$httpBackend_) {
$rootScope = _$rootScope_;
$controller = _$controller_;
$httpBackend = _$httpBackend_;
$scope = $rootScope.$new();
controller = $controller('MyCtrl', { $scope: $scope }, dd1);
}));
//BASIC INFO
describe('should receive sth', function() {
it('finds sth', function() {
expect(controller.answer).toBeDefined();
});
});
});
It really depend on your Controlller implementation.
If you're using scope, you'll probably want to test things on controller's scope, not on the controller itself.
Testing controller with $scope
function MyController($scope, MyService) {
$scope.greetUpperCase = function(name) {
return MyService.greet(name).toUpperCase();
}
}
Basically, when you’re working with $scope, you don’t really care about the controller itself, since it’s doing everything on it’s scope.
So, we will need to:
inject $rootScope
create a new scope
inject it to a new controller
test the $scope
it('test greetUpperCase()', function() {
var myScope = $rootScope.$new();
$controller('MyController', {
$scope: myScope
});
expect(myScope .greetUpperCase('bob')).toBe('HELLO BOB');
});
https://jsfiddle.net/ronapelbaum/pkhaxmdg/
As of recently I have been declaring functions and properties for my angularJS controllers in the following way (app is set to the main apps angular module):
app.controller('myController', ['$scope', function($scope) {
$scope.myProperty = "hello world";
$scope.myFunc = function() {
// do stuff
};
}]);
After a while the controllers' $scope grew to contain many utility functions and properties that are not being used directly in my views and would not be applicable to other controllers, so I changed it to this:
app.controller('myController', ['$scope', function($scope) {
var myProperty = 0, addOne;
addOne = function(i) {
return i++;
};
$scope.myFunc = function() {
myProperty = addOne(myProperty);
// do other stuff
};
}]);
This is working fine but is it okay to declare functions and properties the way shown above or should I extract them out into a service? Can I unit test var declared functions in my controller from jasmine/karma(tried but did not succeed)? What is the best pattern to follow?
I have looked into using the 'this' keyword to accomplish the same thing shown above but I don't think the added value would outweigh the amount of time it would take to convert everything to this pattern. If this is totally wrong please advise a better strategy.
--- Update ---
To answer my own question about testing var declared functions in the controller: How can we test non-scope angular controller methods?
As a general rule, if you're not going to use the variable or function in the view, don't attach it to the scope. So declare your variables and functions within the controller or on the scope based on it's use. You don't want to put unnecessary stuff on the $scope, as this will slow down the digest cycle. As a best practise I follow this format in my controller:
app.controller('MainCtrl', function($scope) {
// controller variables
var age = 123;
// scope variables
$scope.name = 'John';
// controller functions
var helloWorld = function() {
console.log('do work');
}
// scope functions
$scope.isOld = function() {
return age > 50;
}
});
services are singletons while controllers are not.
in addition, services are used to share functionality between many different controllers/directives.
your second code block looks really good to me for internal, controller specific functionality which you don't need to share in other places in your app, and its much better than putting unneeded stuff in the scope.
Can anyone share experience with unit testing views? I read a lot of tutorials about how to do unit testing with views, but everything has some drawbacks.
I came along with the following approach. It works, but I'm wondering if there is a better way to do this. There are also some drawbacks, which I'll explain later on. I'm also doing E2E tests with protractor, but they are always slow, and therefore I limit them to a minimum.
This is my controller. It has two variables bound to its $scope which are used in the view:
// test_ctrl.js
angular.module('app', [])
.controller('TestCtrl', ["$rootScope", "$scope", function ($rootScope, $scope) {
$scope.bar = "TEST";
$scope.jobs = [
{name: "cook"}
];
}]);
The view takes the $scope.bar into a <span> and the $scope.jobs array into an ng-repeat directive:
<!-- test.html the view for this controller -->
<span>
Bar is {{bar || "NOT SET"}}
</span>
<ul>
<li ng-repeat="job in jobs">{{job.name}}</li>
</ul>
And this is the test:
describe('Controller: TestCtrl', function () {
beforeEach(module('templates'));
beforeEach(module('app'));
var TestCtrl, $rootScope, $compile, createController, view, $scope;
beforeEach(inject(function($controller, $templateCache, _$rootScope_, _$compile_, _$httpBackend_) {
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
$compile = _$compile_;
createController = function() {
var html = $templateCache.get('views/test.html');
TestCtrl = $controller('TestCtrl', { $scope: $scope, $rootScope: $rootScope });
view = $compile(angular.element(html))($scope);
$scope.$digest();
};
}));
it('should test the view', function() {
createController();
expect(view.find("li").length).toEqual(1)
console.log($scope.jobs)
});
});
In the beforeEach function, I'll set up the controller. The createController function (which is called from the tests itself) takes a view out of the $templateCache, creates a controller with it's own $scope, then it compiles the template and triggers a $digest.
The template cache is prefilled with karmas preprocessor ng-html2js
// karma.conf.js
...
preprocessors: {
'app/views/*.html': 'ng-html2js'
}
...
With this approach, I have a little problem, and some questions:
1. Additional $$hashKey keys in my objects from ng-repeat
The expect($scope.jobs).toEqual([{name: "cook"}]); in my test throws an error:
Expected [ { name : 'cook', $$hashKey : '009' } ] to equal [ { name : 'cook' } ]
I know that ng-repeat adds these keys, but this is silly to test. The only way around I can think of is separating the controller tests and the view tests. But when I check the jobs array inside my controller, the $$hashKey is not present. Any ideas, why this is happening?
2. $scope problem
When I tried this for the first time, I only had my local scope defined as $scope={} and not $scope = $rootScope.$new(), as I have done in my other controller tests. But with just a plain object as a local scope, I wasn't able to compile it ($compile(angular.element(html))($scope); throwed an error).
I also thought if it is a good idea to pass the $rootScope itself as the current local scope for the controller. Is this a good approach? Or are there any drawbacks, I haven't seen yet?
3. Best practices
I would be very happy to know, how everyone else is doing unit tests in AngularJS. I think views have to be tested, because with all the angular directives, there lies a lot of logic in them, which I would be glad to see waterproofed ;)
I think that what you're doing is a great way to unit test views. The code in your question is a good recipe for someone looking to unit test views.
1. ng-repeat $$hashKey
Don't worry about the data. Instead, test the result of various operations, because that's what you really care about at the end of the day. So, use jasmine-jquery to verify the state of the DOM after creation of the controller, and after simulated click()s, etc.
2. $scope = $rootScope.$new() ain't no problem
$rootScope is an instance of Scope, while $rootScope.$new() creates an instance of ChildScope. Testing with an instance of ChildScope is technically more correct because in production, controller scopes are instances of ChildScope as well.
BTW, the same goes for unit testing directives that create isolated scopes. When you $compile your directive with an instance of ChildScope an isolated scope will be created automatically(which is an instance of Scope). You can access that isolated scope with element.isolateScope()
// decalare these variable here so we have access to them inside our tests
var element, $scope, isolateScope;
beforeEach(inject(function($rootScope, $compile) {
var html = '<div my-directive></div>';
// this scope is an instance of ChildScope
$scope = $rootScope.$new();
element = angular.element(html);
$compile(element)($scope);
$scope.$digest();
// this scope is an instance of Scope
isolateScope = element.isolateScope();
}));
3. +1 Testing Views
Some people say test views with Protractor. Protractor is great when you want to test the entire stack: front end to back end. However, Protractor is slow, and unit testing is fast. That's why it makes sense to test your views and directives with unit tests by mocking out any part of the application that relies on the back-end.
Directives are highly unit testable. Controllers less so. Controllers can have a lot of moving parts and this can make them more difficult to test. For this reason, I am in favor of creating directives often. The result is more modular code that's easier to test.