Testing angular controller: when to create new scope? - javascript

When testing an angular controller, is it not always necessary to create a new scope by calling $rootScope.new()?
Here is my controller:
myControllers.controller("myCtrl1", ['$scope', function($scope) {
$scope.todos = [{"name": "Learn Angular"}, {"name": "Install Karma"}];
$scope.date = '1/1/2014';
}]);
And here is my passing test:
describe("controllers", function() {
var $scope, $rootScope, $controller;
beforeEach(function() {
module("myApp.controllers");
});
beforeEach(inject(function(_$controller_) {
$controller = _$controller_;
//scope = $rootScope.new() **When would you do this?**
}));
//Basic Controller
it("sets todos on scope", function() {
var scope = {}; //**Creating an empty scope object**
$controller("myCtrl1", {$scope : scope});
expect(scope.todos.length).toBe(3);
});
});
I was under the assumption that I need to create a new clean scope every time I test a controller but apparently I was wrong as the above test passes. Any explanations?
Thanks in advance!

In your case it is passing because your controller is only assigning values to the scope, so it can be any object. But for example if the controller had to listen to an event then the test would fail.
$scope.$on('datachange', function(event, args) {
// do something
})
In that case you would have to create a new scope to make your test pass.

It's because you're not using any $scope's methods, like $on or $watch or whatever. Also, to trigger watches in a test, you quite often need to use $scope.$digest(). None of those will work if you pass an empty object as a scope to a controller, of course.
Depends on situation.

Related

Unit Testing Angular 1.5 component that requires ngModel

To test angular 1.5 components, the docs recommend you use ngMock's $componentController instead of using $compile if you don't need to test any of the DOM.
However, my component uses ngModel which I need to pass into the locals for $componentController, but there is no way to programmatically get the ngModelController; the only way to test it is to actually $compile an element with it on it, as this issue is still open: https://github.com/angular/angular.js/issues/7720.
Is there any way to test my components controller without resorting to $compiling it? I also don't want to have to mock the ngModelController myself as its behavior is somewhat extensive and if my tests rely on a fake one rather than the real thing there is a chance newer versions of Angular could break it (though that probably isn't an issue given Angular 1 is being phased out).
tl;dr: Solution is in the third code block.
but there is no way to programmatically get the ngModelController
Not with that attitude. ;)
You can get it programmatically, just a little roundabout. The method of doing so is in the code for ngMock's $componentController service (paraphrased here); use $injector.get('ngModelDirective') to look it up, and the controller function will be attached to it as the controller property:
this.$get = ['$controller','$injector', '$rootScope', function($controller, $injector, $rootScope) {
return function $componentController(componentName, locals, bindings, ident) {
// get all directives associated to the component name
var directives = $injector.get(componentName + 'Directive');
// look for those directives that are components
var candidateDirectives = directives.filter(function(directiveInfo) {
// ...
});
// ...
// get the info of the component
var directiveInfo = candidateDirectives[0];
// create a scope if needed
locals = locals || {};
locals.$scope = locals.$scope || $rootScope.$new(true);
return $controller(directiveInfo.controller, locals, bindings, ident || directiveInfo.controllerAs);
};
}];
Though you need to supply the ngModelController locals for $element and $attrs when you instantiate it. The test spec for ngModel demonstrates exactly how to do this in its beforeEach call:
beforeEach(inject(function($rootScope, $controller) {
var attrs = {name: 'testAlias', ngModel: 'value'};
parentFormCtrl = {
$$setPending: jasmine.createSpy('$$setPending'),
$setValidity: jasmine.createSpy('$setValidity'),
$setDirty: jasmine.createSpy('$setDirty'),
$$clearControlValidity: noop
};
element = jqLite('<form><input></form>');
scope = $rootScope;
ngModelAccessor = jasmine.createSpy('ngModel accessor');
ctrl = $controller(NgModelController, {
$scope: scope,
$element: element.find('input'),
$attrs: attrs
});
//Assign the mocked parentFormCtrl to the model controller
ctrl.$$parentForm = parentFormCtrl;
}));
So, adapting that to what we need, we get a spec like this:
describe('Unit: myComponent', function () {
var $componentController,
$controller,
$injector,
$rootScope;
beforeEach(inject(function (_$componentController_, _$controller_, _$injector_, _$rootScope_) {
$componentController = _$componentController_;
$controller = _$controller_;
$injector = _$injector_;
$rootScope = _$rootScope_;
}));
it('should update its ngModel value accordingly', function () {
var ngModelController,
locals
ngModelInstance,
$ctrl;
locals = {
$scope: $rootScope.$new(),
//think this could be any element, honestly, but matching the component looks better
$element: angular.element('<my-component></my-component>'),
//the value of $attrs.ngModel is exactly what you'd put for ng-model in a template
$attrs: { ngModel: 'value' }
};
locals.$scope.value = null; //this is what'd get passed to ng-model in templates
ngModelController = $injector.get('ngModelDirective')[0].controller;
ngModelInstance = $controller(ngModelController, locals);
$ctrl = $componentController('myComponent', null, { ngModel: ngModelInstance });
$ctrl.doAThingToUpdateTheModel();
scope.$digest();
//Check against both the scope value and the $modelValue, use toBe and toEqual as needed.
expect(ngModelInstance.$modelValue).toBe('some expected value goes here');
expect(locals.$scope.value).toBe('some expected value goes here');
});
});
ADDENDUM: You can also simplify it further by instead injecting ngModelDirective in the beforeEach and setting a var in the describe block to contain the controller function, like you do with services like $controller.
describe('...', function () {
var ngModelController;
beforeEach(inject(function(_ngModelDirective_) {
ngModelController = _ngModelDirective_[0].controller;
}));
});

Using Jasmine to test object initialization within Angular controller

I have a controller that initializes an object upon initialization of the controller, and would like to test that it was called with the specific params that it is actually called with.
I know that I can test that $scope.autoSaveObj has certain properties that would tell me that it in fact initialized, but how would I spy on the initialization event itself?
Essentially, I want to spy on new autoSaveObj just like I would a method.
The main reason I want to test and spy on the object contructor is so that my karma-coverage plugin will show those lines as covered. It won't show the lines as covered if I just test the state of $scope.autoSaveObject after the initialization.
App.controller('ItemCtrl',[ '$scope', function($scope){
$scope.autoSaveObject = new autoSaveObj({
obj: $scope.item,
saveCallback: function() {
return $scope.saveItem();
},
errorCallback: null,
saveValidation: $scope.validItem,
delay: 2000
});
}]);
My guess is the code example is of a partial controller, because properties in the $scope are used which are not initialized here.
Since autoSaveObj is not defined anywhere, I assumed it is a global function. Consider moving this to a service or factory instead.
The following example shows how to
mock autoSaveObj
verify the call parameters, and
verify that the created instance is actually an instance of the correct type.
angular.module('myApp', []).
controller('ItemCtrl', function($scope, $window) {
// Use the injected $window object, so we don't rely on
// the environment and it can be mocked easily.
$scope.autoSaveObject = new $window.autoSaveObj({
obj: $scope.item,
saveCallback: function() {
return $scope.saveItem();
},
errorCallback: null,
saveValidation: $scope.validItem,
delay: 2000
});
});
describe('ItemCtrl', function() {
var $controller;
var $scope;
var $window;
var controller;
beforeEach(module('myApp', function($provide) {
$window = {
// Create an actual function that can be spied on.
// Using jasmine.createSpy won't allow to use it as a constructor.
autoSaveObj: function autoSaveObj() {}
};
// Provide the mock $window.
$provide.value('$window', $window);
}));
beforeEach(inject(function(_$controller_, $rootScope) {
$controller = _$controller_;
$scope = $rootScope.$new();
}));
it('should instantiate an autoSaveObj', function() {
spyOn($window, 'autoSaveObj');
// Initialize the controller in a function, so it is possible
// to do preparations.
initController();
// Do function call expectations as you would normally.
expect($window.autoSaveObj).toHaveBeenCalledWith(jasmine.objectContaining({
saveCallback: jasmine.any(Function),
delay: 2000
}));
// The autoSaveObject is an instance of autoSaveObj,
// because spyOn was used, not jasmine.createSpy.
expect($scope.autoSaveObject instanceof $window.autoSaveObj).toBe(true);
});
function initController() {
controller = $controller('ItemCtrl', {
$scope: $scope
});
}
});
<link href="http://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine.css" rel="stylesheet"/>
<script src="http://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine-html.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/boot.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular-mocks.js"></script>

Unit testing ControllerAs with forms in AngularJS

I have the following situation with Angular:
Controller
function Controller () {
// form used in template
this.form ...
}
Template that has a form and uses this controller
Template
<div ng-controller="Controller as ctrl">
<form name="ctrl.form">
...
</div>
I have to say I'm in general confused why Angular doesn't have a better way of adding the form to the controller other than automatically adding either this.form or $scope.form, depending on how you use controllers, but that's I guess another question.
The real issue I have right now is that I'm not sure how I should test this. If I just instantiate the controller in the test, then my form is undefined
$controller('Controller as ctrl')
I did find a way, by just compiling the template
$scope = $rootScope.$new();
$compile(template)($scope);
But because ng-controller in the template starts a new scope, I can't access the controller directly with $scope.ctrl, instead I'd have to do something like $scope.$$childHead.login
... and I feel it's getting too complicated. edit: not to mention that $$ indicates a 'private' property.
I've solved it myself, but I'm leaving it here unaccepted because I don't think the solution is very nice. If anyone knows a better way, please post.
The problem with compiling templates is that it's also not possible to insert mock services in the controller, at least I didn't figure it out. You get a controller instance on $scope, like $scope.ctrl, but that's it.
The second attempt was to locate just the form in the template, compile and add it to a controller that was instantiated separately. This worked, but not really, because the $scope for the form and controller were different and so any update to a field didn't reflect on the controller state.
The way it works in the end is to instantiate the controller with a $scope
ctrl = $controller('Controller as ctrl', {
someDependency: mockDependency,
$scope: $scope
});
and then to compile the partial template (just the form) with the same $scope
var formTpl = angular.element(template).find('form');
form = $compile(formTpl)($scope);
This way, the controller ends up in $scope.ctrl, but because the form is already named name="ctrl.form" it gets inserted into $scope.ctrl.form and it's visible inside a controller.
When using controllerAs, you can access your form in tests like so:
// 1. Create a new scope
var $scope = $rootScope.$new();
// 2. Run the controller
var Controller = $controller("Controller as ctrl", { $scope: $scope });
// 3. Compile the template against our scope
// This will add property $scope.ctrl.form (assuming that form has name="ctrl.form")
$compile(angular.element(templateHtml))($scope);
// 4. Controller.form should now also be defined
expect(Controller.form).toBeDefined();
Meanwhile mocking can be achieved by using angular's $provide:
beforeEach(angular.mock.module(function ($provide) {
function ApiServiceMock() {
this.getName() = function () {
return "Fake Name";
};
}
// Provide our mocked service instead of 'ApiService'
// when our controller's code requests to inject it
$provide.value("ApiService", ApiServiceMock);
}));
Full example (using both - mocking & form compilation with controllerAs):
Main app code:
angular.module("my.module", [])
.service("ApiService", function () {
this.getName = function () {
return "Real Name";
};
})
.controller("Controller", function (ApiService) {
var ctrl = this;
ctrl.someProperty = ApiService.getName();
});
HTML:
<div ng-controller="Controller as ctrl">
<form name="ctrl.form">
<input type="email" name="email" />
</form>
</div>
Test:
describe("Controller", function () {
var $scope,
Controller;
beforeEach(angular.mock.module("my.module"));
beforeEach(angular.mock.module(function ($provide) {
function ApiServiceMock() {
this.getName() = function () {
return "Fake Name";
};
}
$provide.value("ApiService", ApiServiceMock);
}));
beforeEach(inject(function ($rootScope, $controller, $compile) {
$scope = $rootScope.$new();
Controller = $controller("Controller as ctrl", { $scope: $scope });
// FIXME: Use your own method of retrieving template's HTML instead 'getTemplateContents' placeholder
var html = getTemplateContents("./my.template.html"),
formTpl = angular.element(html).find('form');
$compile(formTpl)($scope);
}));
it('should contain someProperty', function () {
expect(Controller.someProperty).toBeDefined();
});
it('form should contain an email field', function () {
expect(Controller.form.email).toBeDefined();
});
});

$rootScope and passing data from controller to service

I'm using angular js and have got a controller looking like this
myApp = angular.module("myApp.controllers", []);
myApp.controller("ScheduleCtrl", function($scope, $http, ScheduleService){
$scope.hours = 4;
ScheduleService.test();
ScheduleService.initializeSchedule();
});
and a service (in another file) looking like this
myApp = angular.module('myApp.services', []);
myApp.factory('ScheduleService', ['$rootScope', function($rootScope){
return {
test :
function(){
alert("Test");
},
initializeSchedule :
function(){
alert($rootScope.hours);
}
};
});
To assure everyone that things are hooked up properly from service to controller, the first call to "test()" inside my controller produces the desired output in the alert box. However, for the next function, which as of now should be alerting "4", is instead alerting "undefined".
How can I use either $rootScope or something else in order to utilize scope variables to my service.
You need to inject $rootScope into your controller and use $rootScope instead of $scope. DEMO
myApp.controller("ScheduleCtrl", function($scope, $rootScope, $http, ScheduleService){
$rootScope.hours = 4;
ScheduleService.test();
ScheduleService.initializeSchedule();
});
But in this case you don't need to use $rootScope. You can just pass data as parameter into service function.
return {
test :
function(){
alert("Test");
},
initializeSchedule :
function(hours){
alert(hours);
}
};
How to use Angular $rootScope in your controller and view
To share global properties across app Controllers you can use Angular $rootScope. This is another option to share data.
The preferred way to share common functionality across Controllers is Services, to read or change a global property you can use $rootscope.
All other scopes are descendant scopes of the root scope, so use $rootScope wisely.
var app = angular.module('mymodule',[]);
app.controller('Ctrl1', ['$scope','$rootScope',
function($scope, $rootScope) {
$rootScope.showBanner = true;
}]);
app.controller('Ctrl2', ['$scope','$rootScope',
function($scope, $rootScope) {
$rootScope.showBanner = false;
}]);
Using $rootScope in a template (Access properties with $root):
<div ng-controller="Ctrl1">
<div class="banner" ng-show="$root.showBanner"> </div>
</div>
The problem is that $scope is an isolated scope created for your controller. You need to inject the $rootScope into your controller and modify hours on that if you want it to show up in your service.
See more about scope hierarchies here.
controller:
angular.module('myApp', []).controller('ExampleController', ['$scope', '$rootScope', 'ScheduleService', function($scope, $rootScope, ScheduleService) {
$scope.hours = 4;
$rootScope.hours = 42;
ScheduleService.test();
ScheduleService.initializeSchedule();
}]);
service:
angular.module('myApp')
.factory('ScheduleService', function($rootScope) {
return {
test :
function(){
alert("Test");
},
initializeSchedule :
function(childScopeHours){
alert($rootScope.hours);
}
};
});
Working plnkr.

Jasmine angularjs - spying on a method that is called when controller is initialized

I am currently using Jasmine with Karma(Testacular) and Web Storm to write unit test. I am having trouble spying on a method that gets called immediately when the controller is initialized. Is it possible to spy on a method that is called when the controller is initialized?
My controller code, the method I am attempting to spy on is getServicesNodeList().
myApp.controller('TreeViewController', function ($scope, $rootScope ,$document, DataServices) {
$scope.treeCollection = DataServices.getServicesNodeList();
$rootScope.viewportHeight = ($document.height() - 100) + 'px';
});
And here is the test spec:
describe("DataServices Controllers - ", function () {
beforeEach(angular.mock.module('myApp'));
describe("DataServicesTreeview Controller - ", function () {
beforeEach(inject(function ($controller, $rootScope, $document, $httpBackend, DataServices) {
scope = $rootScope.$new(),
doc = $document,
rootScope = $rootScope;
dataServices = DataServices;
$httpBackend.when('GET', '/scripts/internal/servicedata/services.json').respond(...);
var controller = $controller('TreeViewController', {$scope: scope, $rootScope: rootScope, $document: doc, DataServices: dataServices });
$httpBackend.flush();
}));
afterEach(inject(function($httpBackend){
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
}));
it('should ensure DataServices.getServicesNodeList() was called', inject(function ($httpBackend, DataServices) {
spyOn(DataServices, "getServicesNodeList").andCallThrough();
$httpBackend.flush();
expect(DataServices.getServicesNodeList).toHaveBeenCalled();
}));
});
});
The test is failing saying that the method has not been called. I know that I should mock the DataServices and pass that into the test controller. But it seems like I would still have the same problem when spying on that method whether it is a mock or not. Anyone have any ideas or could point me to resources on the correct way to handle this?
When writing unit tests, you should isolate each piece of code. In this case, you need to isolate your service and test it separately. Create a mock of the service and pass it to your controller.
var mockDataServices = {
getServicesNodeList: function () {
return <insert your sample data here > ;
}
};
beforeEach(inject(function ($controller, $rootScope, $document) {
scope = $rootScope.$new(),
doc = $document,
rootScope = $rootScope;
var controller = $controller('TreeViewController', {
$scope: scope,
$rootScope: rootScope,
$document: doc,
DataServices: mockDataServices
});
}));
If it is your service that is making the $http request, you can remove that portion from your unit controller test. Write another unit test that tests that the service is making the correct http calls when it is initialized.

Categories