I am unit testing an Angular directive and would like to mock or stub in some way the instantiation of the named controller in the unit test.
So first I suppose on to some code...
'use strict';
angular.module('App.Directives.BreadCrumbs', [])
.directive('kxBreadcrumbs', function () {
return {
restrict: 'E',
controller: 'BreadCrumbsController',
template:
'<!-- Breadcrumbs Directive HTML -->' +
'<ol class="breadcrumb">' +
' <li ng-repeat="crumb in crumbPath">' +
' <a ng-class="{true: \'disable\', false: \'\'}[crumb.last]" href="{{crumb.href}}" ng-click="updateCrumb(crumb.name)">{{crumb.name}}</a>' +
' </li>' +
'</ol>' +
'<!-- End of Breadcrumbs Driective HTML -->'
};
});
This is one sample directive that I would unit test, the important thing to take away from this is the named controller.
So in my unit test
'use strict';
describe('Directives: Breadcrumbs', function () {
var//iable declarations
elm,
scope,
$rootScope
;
beforeEach(function () {
module('App.Directives.BreadCrumbs');
module('App.Controllers.BreadCrumbs');
module('App.Constants'); // <--- Comes from the controller dependancy
});
beforeEach(inject(function (_$rootScope_, $compile) {
$rootScope = _$rootScope_;
scope = $rootScope.$new();
elm = angular.element('<kx-breadcrumbs></kx-breadcrumbs>');
$compile(elm)(scope);
scope.$apply();
}));
it('Should create the breadcrumbs template', function () {
scope.crumbPath = [{name: 'home', href: '/'},{name: 'coffee', href: '/coffee'},{name: 'milk', href: '/milk'}];
scope.$apply();
var listItem = $(elm).find('li');
expect(listItem.length).toBe(3);
expect($(listItem).text()).toContain('home');
expect($(listItem).text()).toContain('coffee');
expect($(listItem).text()).toContain('milk');
});
});
You can see the inclusion of the 3 modules - the directive, the controller and the third one the constants. This is referenced by the controller as a dependancy so in order to pull this into the unit test I need to pull in the dependancy or in much worse cases the dependancies from the controller. But as I am not unit testing the functionality of the controller in the directive unit test, this seem redundant and bloating of code through inclusion of modules. Ideally I would like to only include the module that I am unit testing.
module('App.Directives.BreadCrumbs');
and not (modules added to exemplify my point more)
module('App.Directives.BreadCrumbs');
module('App.Controllers.BreadCrumbs');
module('App.Constants'); // <--- Comes from the controller dependancy
module('App.Service.SomeService'); // <--- Comes from the controller dependancy
module('App.Service.SomeOtherService'); // <--- Comes from the SomeService dependancy
When we unit test controllers we can mock services that are passed in either completely or by using jasmine spies. Can we accomplish the same sorta thing in unit test of directives so I don't have to follow the dependancy trail?
You can create mocks in module configuration block by using $controllerProvider.register() for controllers, $provide.provider(), $provide.factory(), $provide.service() and $provide.value() for providers, factories and services:
JavaScript
beforeEach(function () {
module('App.Directives.BreadCrumbs', function($provide, $controllerProvider) {
$controllerProvider.register('BreadCrumbsController', function($scope) {
// Controller Mock
});
$provide.factory('someService', function() {
// Service/Factory Mock
return {
doSomething: function() {}
}
});
});
});
Once you do so, Angular will inject your mock BreadCrumbsController controller into kxBreadcrumbs directive. This way you don't need to include real controller and it's dependencies into unit test.
For more information see Angular's official documentation on:
$provide
$controllerProvider
Related
Having trouble reaching my directive's scope for a unit test. Getting generic compile errors when trying to run the unit test.
My app compiles (gulp) and runs fine, and I can unit test non-directive's just fine as well. I am just not sure how to get the test's to work with a directive, I am just trying other people's solutions with some educated guesses.
Main page's HTML and JS
<div company-modal="companyOptions" show-close="false"></div>
.
(function() {
'use strict';
angular
.module('app')
.controller('companyModalCtrl', ['$scope', selectionPage]);
function selectionPage($scope) {
$scope.companyOptions = {};
}
})();
Here is the first portion of my directive (it is very big so just including the important-top part.
(function() {
'use strict';
angular
.module('app')
.directive('companyModal',
companyModal
);
function companyModal() {
return {
restrict: 'A',
replace: false,
transclude: true,
templateUrl: '/src/login/company.html',
scope: {
options: '=companyModal',
showClose: '='
},
bindToController: true,
controller: companySelection,
controllerAs: 'csmvm'
};
}
companySelection.$inject = ['$state'];
function companySelection($state) {
var csmvm = this;
var url;
csmvm.emailList = [];
Here is my attempt at the Unit Test
'use strict';
describe('Company', function () {
var scope;
var controller;
var elem;
beforeEach(module('app'));
beforeEach(inject(function ($controller, $rootScope, $compile) {
scope = $rootScope.$new();
elem = angular.element('<div company-modal="companyOptions" show-close="false"></div>');
$compile(elem)($rootScope);
/*
controller = elem.controller('companyModal as csmvm');
controller = $controller(controller,
{
$scope : scope
});
*/
controller = $controller(elem.controller('companyModal as csmvm'), {
$scope: scope
});
scope.csmvm.emailList.email = "Bob#gmail.com";
}));
describe('Invite', function () {
it('should be an array for emailList', function () {
expect(scope.csmvm.emailList).to.be.an('array');
});
});
});
My problem (and sorry being very undescriptive) is that I just keep getting run-time errors from the test. Such as:
Failed Tests:
Company
"before each" hook: workFn
18) the object { "message": "'undefined' is not an object (evaluating '(isArray(Type) : Type).prototype)".
And again, my app compiles (gulp) and runs fine, and I can unit test non-directive's just fine as well
You need to run expectations towards the isolated scope of your directive.
Right now, scope is referring to the scope where the directive was compiled.
But your directive creates a new isolate scope, and you are not running expectations towards it.
Two ways of doing it:
function isolateScope (el) {
return el.isolateScope();
}
/** or **/
var el, scope, isolateScope;
beforeEach(inject(function ($compile, $rootScope) {
scope = $rootScope.$new();
el = $compile('<directive></directive>')(scope);
scope.$digest();
isolateScope = el.isolateScope();
});
With all of that said, I would remove the controller out of your directive spec suite and test that in isolation. It makes for far cleaner / modular unit tests. Try to break them up as best you can.
I am trying to test an Angular directive that has an external template, but cannot get this to work. This is my first attempt at using Karma. I have Googled for a solution, and tried various changes to the karma.conf.js file, but still keep getting this:
Error: [$injector:modulerr] Failed to instantiate module app/inventory/itemDetails.html due to:
Error: [$injector:nomod] Module 'app/inventory/itemDetails.html' is not available! You either misspelled the module name or forgot to load it.
Folder structure:
app
inventory
itemDetails.html
itemDetailsDirective.js (templateUrl: "app/inventory/itemDetails.html")
UnitTest
karma.conf.js
specs
inventorySpec.js
karma.conf.js
// list of files / patterns to load in the browser
files: [
'../scripts/jquery.min.js',
'scripts/angular.js',
'scripts/angular-mocks.js',
'../app/*.js',
'../app/**/*.js',
'../app/inventory/itemDetails.html',
'specs/*.js'
],
preprocessors: {
'../app/inventory/itemDetails.html':['ng-html2js'] // Is this supposed to be the path relative to the karma.conf.js file?
},
ngHtml2JsPreprocessor: {
stripPrefix: '../',
},
itemDetailsDirective.js
templateUrl: "app/inventory/itemDetails.html",
inventorySpec.js (most stuff commented out for debug purposes)
describe("itemDetailsDirective", function () {
var element, $scope;
beforeEach(module("app/inventory/itemDetails.html"));
beforeEach(inject(function ($compile, $rootScope) {
console.log("itemDetailsDirective");
//element = angular.element('<item-details></item-details>');
//$scope = $rootScope.$new();
//$compile(element)($scope);
//$scope.$digest();
}));
it('should display', function () {
// var isolatedScope = element.isolateScope();
// //expect(isolatedScope.condition).toEqual("Fair");
});
});
So I have a UnitTest folder (VS 2013 project) parallel to the app folder. The paths under "files" in karma.conf.js are correct - all "non-templateUrl" tests work fine.
Help would be great, while I still have some hair left!
I got this working, thanks to this article that I just came across: http://willemmulder.net/post/63827986070/unit-testing-angular-modules-and-directives-with
The key is that the paths are relative to the DISK root!
To illustrate, I changed the karma.conf.js file to use cahceIdFromPath:
ngHtml2JsPreprocessor: {
cacheIdFromPath : function(filepath) {
console.log("karma, cacheIdFromPath " + filepath);
var templateFile = filepath.substr(filepath.indexOf("/app/") + 1 );
console.log("karma, templateFile: " + templateFile);
return templateFile;
},
},
Output:
karma, cacheIdFromPath C:/Dev/BcCom/app/inventory/itemDetails.html
karma, templateFile: app/inventory/itemDetails.html
With the above, it now works as it should!
I am not sure what do you want to test but you are trying to use an html as module which I think is not correct and probably you have to use $httpbackendto mock the GET request. I made a dummy example:
'use strict';
describe('TestMyDirective', function() {
var $compile, $http, $httpBackend, $rootScope, $scope, template;
beforeEach(module('myApp'));
beforeEach(inject(function($injector) {
$rootScope = $injector.get('$rootScope');
$compile = $injector.get('$compile');
// Inject $httpBackend service
$httpBackend = $injector.get('$httpBackend');
// Mock request to your html
$httpBackend.whenGET('my.html').respond("GotIT");
$scope = $rootScope.$new();
template = $compile("<my-directive> </my-directive>")($scope);
$scope.$digest();
}));
/*
*
* Tests
*
*/
describe('Test something', function() {
it('should test something', function() {
// You can test that your directive add something on the $scope
});
});
});
I have a very simple controller that looks like this.
timeInOut.controller('timeInOutController', function($scope, $filter, $ionicScrollDelegate){
...
});
Whenever I try to create a unit test for it like so...
(function() {
'use strict';
var scope, controller, filter;
describe('timeInOutController', function () {
beforeEach(module('common.directives.kmDateToday'));
beforeEach(inject(function ($rootScope, $controller, $filter) {
scope = $rootScope.$new();
filter = $filter;
controller = $controller('timeInOutController', {
$scope: scope
});
}));
describe('#date setting', function(){
...
});
});
})();
I get the error:
[$injector:unpr] Unknown provider: $ionicScrollDelegateProvider <- $ionicScrollDelegate
Obviously in my example here I'm not trying to inject the $ionicScrollDelegate into the test, that's just because I've tried it any number of ways with no success and don't know which failed attempt to include.
Also in my karma.conf.js file I am including the ionic.bundle.js and angular-mocks.js libraries/files.
I can successfully unit test anything that doesn't use anything $ionic in it, so I know my testing framework is set up correctly, the issue is injecting anything ionic related.
You need to pass in all the parameters if you're going to instantiate your controller via angular. By adding the parameters you are telling angular that any time you create one of these controllers I need these things too because I am dependent upon them.
So my suggestion is to mock up some representation of these dependencies and inject them in when you are creating the controller. They do not have to be (and should not be) the actual services for your unit tests. Jasmine gives you the ability to create spy objects that you can inject so you can verify the the behavior of this unit.
(function() {
'use strict';
var scope, controller, filter, ionicScrollDelegate;
describe('timeInOutController', function () {
beforeEach(module('common.directives.kmDateToday'));
beforeEach(inject(function ($rootScope, $controller, $filter) {
scope = $rootScope.$new();
filter = $filter;
// func1 and func2 are functions that will be created as spies on ionicScrollDelegate
ionicScrollDelegate = jasmine.createSpyObj('ionicScrollDelegate', ['func1', 'func2']
controller = $controller('timeInOutController', {
$scope: scope,
$filter: filter,
$ionicScrollDelegate: ionicScrollDelegate
});
}));
describe('#date setting', function(){
...
});
});
})();
You can find more about spies via jasmine's documentation
You need to create mock objects for all dependencies your controller is using.
Take this controller as an example:
angular.module('app.module', [])
.controller('Ctrl', function($scope, $ionicLoading) {
$ionicLoading.show();
});
Here you are using the $ionicLoading service, so if you want to test this controller, you have to mock that object specifying the methods you're using in the controller:
describe('Test', function() {
// Mocks
var $scope, ionicLoadingMock;
var ctrl;
beforeEach(module('app.module'));
beforeEach(function() {
// Create $ionicLoading mock with `show` method
ionicLoadingMock = jasmine.createSpyObj('ionicLoading', ['show']);
inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
ctrl = $controller('Ctrl', {
$scope: $scope,
$ionicLoading: ionicLoadingMock
});
});
});
// Your test goes here
it('should init controller for testing', function() {
expect(true).toBe(true);
});
});
Here is the directive:
app.directive('templates',function() {
return {
restrict:'E',
templateUrl: function(s,e) {
switch (e.template) {
case 'temp1':
return 'temp1.html';
case 'temp2':
return 'temp1.htm2';
default:
// do nothing... ;
}
}
};
});
I can get it to compile in my test but i'm not sure how to test if the correct templates are being called
There is not much to test here. But as a feel good test you can just load a template into cache and do a test if specific element has been rendered or not as a feel good test.
Example:-
describe('templates', function () {
beforeEach(inject(function ($rootScope, $templateCache, $compile) {
// Set an arbitrary template to test
$templateCache.put('temp1.html', '<div class="test">Hello</div>');
element = angular.element("<templates template='temp1'></templates>");
$compile(element)(scope);
$rootScope.$digest();
}));
it('Should load template', function () {
expect(element.find('.test').length).toEqual(1); //Test if element has loaded template properly
expect(element.find('.test').text()).toEqual("Hello");
});
Demo
On a different note your directive can break if there is no template provided, it is required to return a template from templateurl function. Also you can make this simple directive more generic as well.
.directive('templates',function() {
return {
restrict:'E',
templateUrl: function(e,attr) {
return attr.template + ".html"
};
});
How ever, there is nothing to be tested here because you will just end up testing angular's templateUrl function evaluation.
I have a service, DataContext which returns me a set of data that I want to use in my controller. This data is used by an ng-grid directive. The options for the grid are supplied by GridOptionsService.
All of this works fine, but I'm trying to write a unit test to check and see if everything's working.
describe('Grid display test', function() {
var $scope, elm, oCtrl;
beforeEach(module('ngGrid'));
beforeEach(inject(function ($rootScope, $compile, $controller,DataContext,GridOptionsService) {
$scope = $rootScope.$new();
$scope.gridOptions = GridOptionsService.getGridOptions('documents');
$scope = $rootScope;
elm = angular.element('<div ng-grid="gridOptions"></div>');
oCtrl = $controller('Repository',{$scope: $scope});
$compile(elm)($scope)
DataContext.getDocuments().then(function(data){
$scope.myData = data;
console.log('here are the grid options: ')
console.log($scope.gridOptions);
})
}));
it('should display rows',function(done){
inject(function($rootScope, $compile, $controller, DataContext,GridOptionsService){
$rootScope.$apply(function(){
DataContext.getDocuments().then(function(data){
expect(data.length).toBe(1000);
done();
})
})
})
})
});
DataContext.getDocuments returns a promise and I use that to set the myData variable of the controller. This data is the data for the grid.
$scope.gridOptions = GridOptionsService.getGridOptions('documents');
gridOptions is simply a JS object returned from a service. When I run the test I get the error: Error: [$injector:unpr] Unknown provider: DataContextProvider <- DataContext
All of the scripts that should be included in the spec runner are, and the code definitely works, but I just don't know how to test it.
How can I test AJAX code that changes the appearance of my DOM with Jasmine?
Try mocking out your custom dependencies for your Jasmine test. The [$injector:unpr] results from the $injector being unable to resolve a required dependency. You can use $provide to register components with the injector. Try to add something like...
beforeEach(function () {
mockDependency = {
getDataSet: function () {
return 'mockDataSet';
}
};
module(function ($provide) {
$provide.value('DataContext', mockDependency);
});
});
Here's the documentation for $provide...https://docs.angularjs.org/api/auto/object/$provide
Hope that helps