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('');
}));
});
Related
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;
}));
});
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 a factory defined like this:
angular.module("myServices")
.factory("$service1", ["$rootScope", "$service2", function($rootScope, $service2){...})];
Now, I want to test it, but just injecting $service1 is not working because i get an 'unknown provider' error. So I tried something like that. But I still can't make it work. Why?
beforeEach(function() {
module("myServices");
inject(function ($injector) {
dependencies["$service2"] = $injector.get("$service2");
});
module(function($provide) {
$provide.value("$service1", dependencies["$service2"]);
});
inject(function($injector) {
factory = $injector.get("$service1");
});
});
This is what's working in my tests, using underscores:
describe('Service: $service1', function () {
var $service2, scope;
beforeEach(inject(function (_$service2_, $rootScope) {
$service2 = _$service2_;
scope = $rootScope;
}));
//tests
});
If that still doesn't work, then maybe you're not loading the relevant files (such as service2.js) in your tests.
I'm trying to understand how to unit test my directive in my situation below.
Basically I'm trying to unit test a directive which has a controller. On the loading of this directive the controller makes a http request by a service which brings some data to the controller again then provides this data to the directive view.
On the scenario below in my understanding I should do:
A $httpBackend to avoid an exception when the http request is done;
Populate the fake data to be able to unit test the directive with diff behaviors
Compile the directive
What I've been trying so far, as you can see, is override the Service with the fake data. What I could not make work so far.
Some doubts come up now.
As you can see in my Controller. I'm providing the whole Service to the view:
$scope.ItemsDataservice = ItemsDataservice;
What makes me believe that my approach to override the Service should work.
My question:
On scenario below I understand that I could override the Service to manipulate the data or even override the controller to manipulate the data by scope.
What's the right thing to do here?
Am I understand wrong?
Am I mixing the unit tests?
In my current unit test code, when I'm applying the fake data(or not), is not make any difference:
ItemsDataservice.items = DATARESULT;
ItemsDataservice.items = null;
Controller:
angular.module('app')
.controller('ItemsCtrl', function ($scope, $log, ItemsDataservice) {
$scope.ItemsDataservice = ItemsDataservice;
$scope.ItemsDataservice.items = null;
$scope.loadItems = function() {
var items = [];
ItemsDataservice.getItems().then(function(resp) {
if (resp.success != 'false') {
for (resp.something ... ) {
items.push({ ... });
};
ItemsDataservice.items = items;
};
}, function(e) {
$log.error('Error', e);
});
};
$scope.loadItems();
});
Service:
angular.module('app')
.service('ItemsDataservice', function ItemsDataservice($q, $http) {
ItemsDataservice.getItems = function() {
var d = $q.defer();
var deffered = $q.defer();
var url = 'http://some-url?someparameters=xxx'
$http.get(url)
.success(function (d) {
deffered.resolve(d);
});
return deffered.promise;
};
return ItemsDataservice;
});
Directive:
angular.module('app')
.directive('items', function () {
return {
templateUrl: '/items.html',
restrict: 'A',
replace: true,
controller: 'ItemsCtrl'
};
});
Unit testing directive:
ddescribe('Directive: Items', function () {
var element, scope, _ItemsDataservice_, requestHandler, httpBackend;
var URL = 'http://some-url?someparameters=xxx';
var DATARESULT = [{ ... }];
// load the directive's module
beforeEach(module('app'));
beforeEach(module('Templates')); // setup in karma to get template from .html
beforeEach(inject(function ($rootScope, ItemsDataservice) {
httpBackend = $httpBackend;
scope = $rootScope.$new();
_ItemsDataservice_ = ItemsDataservice;
requestHandler = httpBackend.when('GET', URL).respond(200, 'ok');
}));
afterEach(function() {
//httpBackend.verifyNoOutstandingExpectation();
//httpBackend.verifyNoOutstandingRequest();
});
it('Show "No Items available" when empty result', inject(function ($compile) {
_ItemsDataservice_.items = null;
element = angular.element('<div data-items></div>');
element = $compile(element)(scope);
scope.$digest();
element = $(element);
expect(element.find('.msg_noresult').length).toBe(1);
}));
it('Should not show "No Items available" when data available ', inject(function ($compile) {
_ItemsDataservice_.items = DATARESULT;
element = angular.element('<div data-items></div>');
element = $compile(element)(scope);
scope.$digest();
element = $(element);
expect(element.find('.msg_noresult').length).toBe(0);
}));
});
I sorted out the problem.
Changed this line:
element = $compile(element)(scope);
To this line:
element = $compile(element.contents())(scope);
The only diff is the jquery method .contents()
I did not get yet why. But it solved.
Update:
Another thing I've just discovered and that was really useful for me.
You can use regular expression on you httpBackend:
httpBackend.whenGET(/.*NameOfThePageXXX\.aspx.*/).respond(200, 'ok');
So, you don't need to worry to use exactly the same parameters etc if you just want to avoid an exception.
I'm getting this error. It's something along the lines of me injector being unable to resolve a required dependency, but even with my limited knowledge of angular I'm pretty sure this code shouldn't be depending on any modules.
This code works perfectly fine in the browser, however it doesn't seem to want to work in my test. I've been following the examples from the documentation
My angular version is 1.2.13 (edit: now using 1.12.15).
Here's my code:
var app = angular.module('app', [])
.controller('GreetingCtrl', function ($scope) {
$scope.title = "Hello World!";
$scope.message = "Test, test. One? Two?";
});
Here's the jasmine test that's failing.
describe('app controllers', function () {
beforeEach(module('app'));
describe('GreetingCtrl', function () {
it('should says hello world', inject(function ($controller) {
var $scope = {};
$controller('GreetingCtrl', $scope);
expect($scope.title).toBe("Hello World!");
}));
});
});
I don't believe it's even gotten to the point of running my test because it fails before even running it. I believe I've correctly concatenated the files correctly as well. Here's the error I've received from the jasmine test runner.
Error: [$injector:unpr] http://errors.angularjs.org/1.2.13/$injector/unpr?p0=%24scopeProvider%20%3C-%20%24scope (line 4569) (1)
Edit: tried upgrading to 1.12.15, nothing has changed.
So apparently after a little chat on the IRC, the documentation might be out of date, & this is a working solution. I was linked to this solution, & corrected my test accordingly.
describe('app controllers', function () {
var ctrl, scope;
beforeEach(module('app'));
describe('GreetingCtrl', function () {
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller('GreetingCtrl', {$scope: scope});
}));
it('should says hello world', function () {
expect(scope.title).toBe("Hello World!");
});
});
});
Edit:
I accidentally misread the docs originally & here's a cleaner solution closer to the docs.
describe('app controllers', function () {
beforeEach(module('app'));
describe('GreetingCtrl', function () {
it('should says hello world', inject(function ($controller) {
var $scope = {};
// this is the line that caused me pain
$controller('GreetingCtrl', { $scope: $scope });
expect($scope.title).toBe("Hello World!");
}));
});
});