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;
}));
});
Related
after two days of researching and testing, I need to ask for help.
I am trying to test a directive using Jasmine, and I don't want to include the Karma engine.
In order to see the result of the test, I use the jasmine-html library, and the jasmine css.
I am able to test services easily, but when it comes to testing directive I am not able to access to the isolated scope. One thing to keep in mind is that I don't want to use controllers in my directive, but link functions.
(according to Angular doc, controller should be used only when you want to expose an API to other directives.)
I found multiple answers on StackOverflow, but none of them worked.
The last thing I tried is based on this answered question StackOverflow.
This is the test I am trying to replicate
describe('Wikis Directive Test Suite', function () {
var $scope, scope, elem, directive, linkFn, html;
beforeEach(module('app'));
beforeEach(function () {
html = '<wikis></wikis>';
inject(function ($compile, $rootScope, $templateCache) {
$templateCache.put('templates/wiki-list.html', '<div>wiki template</div>');
$scope = $rootScope.$new();
$scope.wikis = [];
elem = angular.element(html);
elem= $compile(elem)($scope);
//scope = elem.isolateScope(); /*This is always undefined!*/
scope = elem.scope(); /* this doesn't have addWiki, only the empty wikis array */
$rootScope.$digest();
});
});
it('add Wiki should add a valid wiki URL to artist', function () {
var url = 'http://www.foo.com';
scope.newWikiURL = url;
scope.addWiki();
expect(scope.wikis.length).toBe(1);
expect(scope.wikis[0]).toBe(url);
expect(scope.newWikiURL).toBe('');
});
});
The "only" difference, is that I need to use Jasmine 2.4.1 and Angular 1.0.7. Unfortunately it seems like with these libraries the test doesn't work.
The thing is that isolateScope() is always undefined!
I've created a plunker to reproduce the problem.
https://plnkr.co/edit/PRt350VlASShg5oVeY88
Ok, I (actually one of my colleagues found it) what I was doing wrong!
This is the right code which works.
describe('Wikis Directive Test Suite', function () {
var $scope, scope, elem, directive, linkFn, html;
beforeEach(module('app'));
beforeEach(function () {
html = '<wikis></wikis>';
inject(function ($compile, $rootScope, $templateCache) {
$templateCache.put('templates/wiki-list.html', '<div>wiki template</div>');
$scope = $rootScope.$new();
$scope.wikis = [];
elem = angular.element(html);
elem = $compile(elem)($scope);
$scope.$digest();
scope = elem.isolateScope();
});
});
it('add Wiki should add a valid wiki URL to artist',inject( function () {
var url = 'http://www.foo.com';
scope.newWikiURL = url;
scope.addWiki();
expect(scope.wikis.length).toBe(1);
expect(scope.wikis[0]).toBe(url);
expect(scope.newWikiURL).toBe('');
}));
});
I have read a couple articles and SO questions, but am still a little fuzzy on what I can and can't unit test.
I have a directive which returns a controller that has a couple of functions. Some of these functions have return statements, and others don't. I can see in the code coverage report that all of the functions are available to test, but only the functions with return statements are covered. Is it possible to unit test the controller's functions that don't have return statements? If yes, then how would I go about doing so?
directive.js snippet
app.directive('directive', function() {
var theController = ['$scope', function($scope) {
$scope.add = function() {
// no return statement, not covered, but is available
};
$scope.disableAddButton = function() {
// has a return statement and is fully covered
};
}];
return: {
scope: {
args: '='
},
templateUrl: ...,
restrict: 'A',
controller: theController
};
});
directive.spec.js
describe('The directive', function() {
var element,
$scope,
controller;
beforeEach(module('app'));
beforeEach(module('path/to/template.html'));
beforeEach(inject(function($compile, $controller, $rootScope, $templateCache) {
template = $templateCache.get('path/to/template.html');
$templateCache.put('path/to/template.html', template);
$scope = $rootScope;
controller = $controller;
var elm = angular.element('<div directive></div>');
element = $compile(elm)($scope);
$scope.$digest();
theController = element.controller('directive', {
$scope: $scope
});
}));
it('should compile', function() {
expect(element.html()).not.toBeNull();
});
describe('$scope.add', function() {
beforeEach(inject(function() {
add = theController.add();
}));
it('should be defined', function() {
expect(add).toBeDefined(); // passes
});
// Now what???
});
});
The "unit" you're testing is the entire directive/controller, not individual functions. Rather than trying to test each function in isolation, test that the results of calling the function are what you expect.
For example, what does add do? Presumably it has an effect on something - ensure that that has taken place.
Your title also mentions private functions. These are what the implementor of the "unit" has decided are necessary to get their job done. They aren't part of the public interface of the object, so you shouldn't need to worry about testing them - just ensure that the unit does what it's public interface says it should do - there could be any number of private functions actually doing that work.
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();
});
});
The problem
I am unit testing a directive has no controller or template, only a link function. The directive requires ngModel and calls one of its functions in the link function. I want to spy on ngModel in my unit tests to ensure the right function is being called.
The code
Directive:
angular.module('some-module').directive('someDirective', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attr, controller) {
controller.doSomething(); //Calls some random function on the ngModel controller
}
};
});
What I've tried
I've tried to inject a spy ngModel like this:
beforeEach(module(function($provide) {
$provide.factory('ngModelDirective', function() {
return {};
});
$provide.factory('ngModelController', function() {
return function() {};
});
}));
As I discovered on this question, trying to override built-in properties causes an error to be thrown, and is bad practice.
So then I tried to test the directive the way the Angular docs say to:
var $scope = $rootScope.$new();
var element = $compile('<div some-directive></div>')($scope);
And spy on NgModelController like this:
var ngModelControllerSpyDoSomething = sinon.spy(element.controller('ngModel'), 'doSomething');
But this doesn't work, because one $compile is run, it executes the link function, so I'm not spying on it until it's too late (the spy is coming back as never having been called). This is also the case if I put $scope.$digest(); before or after creating the spy.
You will have to add your spy to the $scope you are injecting into the $compile-Function and then link it within the actual directives HTMLngModel`, so:
var $scope = $rootScope.$new();
$scope.mySpy = // create a stub function with sinon here
var element = $compile('<div some-directive ng-model="mySpy"></div>')($scope);
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.