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.
Related
I'm trying to test a directive using karma and jasmine. I'm using angular 1.4 and I searched different things here in SO and throuhg internet but I can't make it work.
var angular = require('angular');
module.exports = angular.module('myApp.ui.apps.servicesObject.list', [])
.directive('servicesObjectList', function(){
return {
restrict: 'E',
replace: true,
scope: true,
bindToController: {
services: '=',
selectedServices: '='
},
templateUrl: 'app/ui/apps/services/directives/services.html',
controllerAs: 'servicesListCtrl',
controller: 'ServicesListController'
}
})
.controller('ServicesListController', require('./servicesListController'));
This is how I'm trying to test it.
describe('Service app test, listDirective' , function(){
var element, scope, controller;
beforeEach(function(){
angular.mock.module('templates');
angular.mock.module('myApp.ui.apps.servicesObject.list', function($provide) {
$provide.value('gettextCatalog', { getString: function(){}});
$provide.value('translateFilter', function(){});
});
});
beforeEach(inject(function($rootScope, $compile, $controller){
scope = $rootScope;
scope.services= _servicesMock_;
element = '<services-object-list selected-services="[]" services="services"></services-object-list>';
$compile(element)(scope);
scope.$digest();
controller = $controller('ServicesListController', {$scope: scope});
console.log(controller.getServices());
}));
it ("First test", function(){
expect(true).toBe(true);
});
});
The problem that I have is that services is not binding in my controller, only in the scope. What I'm doing wrong? If I do console(controller.getServices()). It returns me undefined instead of the services that I pass as attribute. My production code is working as expected but not tests.
Thank you very much!
After some hours I just discover a new feature added in angular 1.3 to make binding in unitTesting easier. Here is the thread of the discussion https://github.com/angular/angular.js/issues/9425
Basically a third argument is added to the controller constructor service where you can pass the data that is bind to controller.
So the unitTest configuration will be like this.
describe('Service app test, listDirective' , function(){
var element, scope, controller;
beforeEach(function(){
angular.mock.module('templates');
angular.mock.module('myApp.ui.apps.servicesObject.list', function($provide) {
$provide.value('gettextCatalog', { getString: function(){}});
$provide.value('translateFilter', function(){});
});
});
beforeEach(inject(function($rootScope, $compile, $controller){
var data = {
services: _servicesMock_
};
scope = $rootScope;
scope.services= _servicesMock_;
element = '<services-object-list selected-services="[]" services="services"></services-object-list>';
$compile(element)(scope);
scope.$digest();
controller = $controller('ServicesListController', {$scope: scope}, data);
console.log(controller.getServices());
}));
it ("First test", function(){
expect(true).toBe(true);
});
});
When I have one controller attached to a module, I can use mocha, karma to test it successfully. But when I add two controllers to the same module, the tests fail. Why is that?
I have 2 controllers defined on the same module. I can manually test the controllers and they work.
src/app/itemListController.js
angular.module('lazyLoad', [])
.controller('ItemListController', ['$scope', function ($scope) {
...
}]);
src/app/invoiceController.js
angular.module('lazyLoad', [])
.controller('InvoiceController', ['$scope', function ($scope) {
...
}]);
And 2 unit-tests:
test/app/itemListController.mocha.js
'use strict';
describe('testing movies', function () {
var scope;
var fixture;
beforeEach(module('lazyLoad'));
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
fixture = $controller('ItemListController', {$scope: scope});
}));
it('....', function() {});
});
test/app/invoiceController.mocha.js
'use strict';
describe('testing movies', function () {
var scope;
var fixture;
beforeEach(module('lazyLoad'));
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
fixture = $controller('InvoiceController', {$scope: scope});
}));
it('....', function() {});
});
I get:
PhantomJS 1.9.8 (Mac OS X 0.0.0) testing movies "before each" hook: workFn FAILED
the object {
"line": 1761
"message": "[ng:areq] Argument 'ItemListController' is not a function, got undefined
http://errors.angularjs.org/1.4.1/ng/areq?p0=ItemListController&p1=not%20a%20function%2C%20got%20undefined"
"name": "Error"
Now if I change the module-name for the invoiceController.js and invoiceController.mocha.js to say invoiceM then both tests work.
I must be doing something wrong...
You define your modules twice. When you use brackets [] to pass empty dependencies, you actually create a module and replace an old one if exists with the same name. What you need to do is:
// Create the module, maybe in a separate place.
angular.module('lazyLoad', []);
// Attach controllers to that module:
angular.module('lazyLoad') // HEY! SEE THIS? NO BRACKETS.
.controller('ItemListController', ...);]
angular.module('lazyLoad') // HEY! SEE THIS? NO BRACKETS.
.controller('InvoiceController' ...);
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);
});
});
I'm trying to test my application with Karma but I get following Errors:
minErr/<#/home/usr/webui-ng/src/client/app/bower_components/angular/angular.js:78:5
loadModules/<#/home/usr/webui-ng/src/client/app/bower_components/angular/angular.js:3859:1
forEach#/home/usr/webui-ng/src/client/app/bower_components/angular/angular.js:325:7
loadModules#/home/usr/webui-ng/src/client/app/bower_components/angular/angular.js:3824:5
createInjector#/home/usr/webui-ng/src/client/app/bower_components/angular/angular.js:3764:3
workFn#/home/usr/webui-ng/src/client/app/bower_components/angular-mocks/angular-mocks.js:2150:9
These are my files:
hello.js
angular.module('myApp', [])
.controller('MainController', function($scope) {
$scope.name = "Ari";
$scope.sayHello = function() {
$scope.greeting = "Hello " + $scope.name;
}
})
hello.js - test file :
describe('Unit: MainController', function() {
// Load the module with MainController
beforeEach(module('myApp'));
var ctrl, scope;
// inject the $controller and $rootScope services
// in the beforeEach block
beforeEach(inject(function($controller, $rootScope) {
// Create a new scope that's a child of the $rootScope
scope = $rootScope.$new();
// Create the controller
ctrl = $controller('MainController', {
$scope: scope
});
}));
it('should create $scope.greeting when calling sayHello',
function() {
expect(scope.greeting).toBeUndefined();
scope.sayHello();
expect(scope.greeting).toEqual("Hello Ari");
});
});
As you can see, I've already loaded the module, as the solution was in Controller undeclared in Jasmine test.
What else could it be? I haven't found much about these errors on the web. I would be very happy to finally find an answer for that.
As runTarm mentioned, the path in the karma.conf.js was not set to the actual path of the script file (hello.js).
This is a somewhat of a follow-on question to this one: Mocking $modal in AngularJS unit tests
The referenced SO is an excellent question with very useful answer. The question I am left with after this however is this: how do I unit test the modal instance controller? In the referenced SO, the invoking controller is tested, but the modal instance controller is mocked. Arguably the latter should also be tested, but this has proven to be very tricky. Here's why:
I'll copy the same example from the referenced SO here:
.controller('ModalInstanceCtrl', function($scope, $modalInstance, items){
$scope.items = items;
$scope.selected = {
item: $scope.items[0]
};
$scope.ok = function () {
$modalInstance.close($scope.selected.item);
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
});
So my first thought was that I would just instantiate the controller directly in a test, just like any other controller under test:
beforeEach(inject(function($rootScope) {
scope = $rootScope.$new();
ctrl = $controller('ModalInstanceCtrl', {$scope: scope});
});
This does not work because in this context, angular does not have a provider to inject $modalInstance, since that is supplied by the UI modal.
Next, I turn to plan B: use $modal.open to instantiate the controller. This will run as expected:
beforeEach(inject(function($rootScope, $modal) {
scope = $rootScope.$new();
modalInstance = $modal.open({
template: '<html></html>',
controller: 'ModalInstanceCtrl',
scope: scope
});
});
(Note, template can't be an empty string or it will fail cryptically.)
The problem now is that I have no visibility into the scope, which is the customary way to unit test resource gathering, etc. In my real code, the controller calls a resource service to populate a list of choices; my attempt to test this sets an expectGet to satisfy the service my controller is using, and I want to validate that the controller is putting the result in its scope. But the modal is creating a new scope for the modal instance controller (using the scope I pass in as a prototype), and I can't figure out how I can get a hole of that scope. The modalInstance object does not have a window into the controller.
Any suggestions on the "right" way to test this?
(N.B.: the behavior of creating a derivative scope for the modal instance controller is not unexpected – it is documented behavior. My question of how to test it is still valid regardless.)
I test the controllers used in modal dialogs by instantiating the controller directly (the same way you initially thought to do it above).
Since there there's no mocked version of $modalInstance, I simply create a mock object and pass that into the controller.
var modalInstance = { close: function() {}, dismiss: function() {} };
var items = []; // whatever...
beforeEach(inject(function($rootScope) {
scope = $rootScope.$new();
ctrl = $controller('ModalInstanceCtrl', {
$scope: scope,
$modalInstance: modalInstance,
items: items
});
}));
Now the dependencies for the controller are satisfied and you can test this controller like any other controller.
For example, I can do spyOn(modalInstance, 'close') and then assert that my controller is closing the dialog at the appropriate time.
Alternatively, if you're using jasmine, you can mock the $uibModalInstance using the createSpy method:
beforeEach(inject(function ($controller, $rootScope) {
$scope = $rootScope.$new();
$uibModalInstance = jasmine.createSpyObj('$uibModalInstance', ['close', 'dismiss']);
ModalCtrl = $controller('ModalCtrl', {
$scope: $scope,
$uibModalInstance: $uibModalInstance,
});
}));
And test it without having to call spyOn on each method, let's say you have 2 scope methods, cancel() and confirm():
it('should let the user dismiss the modal', function () {
expect($scope.cancel).toBeDefined();
$scope.cancel();
expect($uibModalInstance.dismiss).toHaveBeenCalled();
});
it('should let the user confirm the modal', function () {
expect($scope.confirm).toBeDefined();
$scope.confirm();
expect($uibModalInstance.close).toHaveBeenCalled();
});
The same problem is with $uidModalInstance and you can solve it in similar way:
var uidModalInstance = { close: function() {}, dismiss: function() {} };
$ctrl = $controller('ModalInstanceCtrl', {
$scope: $scope,
$uibModalInstance: uidModalInstance
});
or as said #yvesmancera you can use jasmine.createSpy method instead, like:
var uidModalInstance = jasmine.createSpyObj('$uibModalInstance', ['close', 'dismiss']);
$ctrl = $controller('ModalInstanceCtrl', {
$scope: $scope,
$uibModalInstance: uidModalInstance
});
Follow below given steps:
Define stub for ModalInstance like give below
uibModalInstanceStub = {
close: sinon.stub(),
dismiss: sinon.stub()
};
Pass the modal instance stub while creating controller
function createController() {
return $controller(
ppcConfirmGapModalComponentFullName,
{
$scope: scopeStub,
$uibModalInstance: uibModalInstanceStub
});
}
});
Stub methods close(), dismiss() will get called as part of the tests
it('confirm modal - verify confirm action, on ok() call calls modalInstance close() function', function() {
action = 'Ok';
scopeStub.item = testItem;
createController();
scopeStub.ok();
});