Angular unit testing initialize "nameless" controller in directive - javascript

I'm curious as to how I would go about unit testing what I think is an anonymous controller inside of a directive.
directive.js
app.directive('directive',
function() {
var controller = ['$scope', function($scope) {
$scope.add = function() { ... };
}];
return {
restrict: 'A',
scope: {
args: '='
},
templateUrl: '...',
controller: controller
};
}
};
Is a controller defined as such able to be unit tested? I have tried to initialize it several different ways. Currently I have it setup like this:
describe('The directive', function() {
var element,
scope,
controller;
var args = {
...
}
beforeEach(module('app'));
beforeEach(module('path/to/template.html'));
beforeEach(function() {
inject(function($compile, $rootScope, $controller) {
scope = $rootScope.$new();
scope.args = args;
element = angular.element('<div directive></div>');
template = $compile(element)(scope);
scope.$digest();
controller = element.$controller;
});
});
// assertions go here
});
I keep getting TypeError: 'undefined' is not an object (evaluating ...) errors, so I don't think I am initializing the controller correctly. I mainly want to know if something like this is unit testable without changing the directive's source code at all.

I'm not sure if what you are trying to do is possible. However, I do know that there is a much easier way and that is to make it a standard controller. (You seem to be aware of this already but it's worth pointing out.)
The logic in a controller really shouldn't be dependent on the directive anyway so by making a named controller you are separating concerns which is a good thing. You can even see this used in recommended style guides for AngularJS. Once you have the controller set up properly you shouldn't have any issues testing it. Splitting it out like that also helps in doing proper dependency injection making for simpler code and simpler tests.

Related

How to Test $scope.$on in an AngularJS 1.5+ Component Using Jasmine

I'm trying to test that some code gets executed using $scope.$on in an AngularJS 1.5+ component. I'm not sure how to set up the $rootScope correctly in Jasmine so I can execute the broadcast. I'm using this stackoverflow page and this blog as a reference. Here is my code.
// Component
(function (app) {
app.component('demoComponent', {
controller: ['$scope' function ($scope) {
$scope.$on('someBroadcast', function (data) {
// do something with data...
});
}]
});
})(angular.module('demoApp'));
// Jasmine setup
var ctrl, $rootScope, $componentController;
beforeEach(function () {
module('demoApp');
inject(function ($rootScope, _$componentController_) {
ctrl = $rootScope.$new();
$componentController = _$componentController_('demoComponent', { $scope: ctrl }, null);
});
});
My code breaks down in the inject function in the Jasmine setup. Does anyone know what I need to change to get this working?
Since ctrl is the scope that you will be using inside of your controller, you can do this:
ctrl.$broadcast('someBroadcast', 'test', 'values, 123);
And as a side note, usually you will name your scopes something like scope or $scope, or maybe event controllerScope. ctrl implies that the object is a controller, which it is not.

How to load actual dependencies into a Angularjs test with Jasmine and Karma?

I have been looking around online, but seems, no one really injecting the actual dependencies into a unit test for Angularjs using Jasmine and Karma.
I think there is definitely a separation of concern for the testing process, but I also would like to know how it integrated well with current dependencies in use... so just in case a dependencies is not working well with my component, I will be aware of it!
So, I wonder how can I inject the actual dependencies? So far I found online articles are all about mocking it with a fake one... But I want to use the actual one. Right now, when I enter karma start I am getting a error of Error: [$injector:unpr] Unknown provider: _Provider <- _ <-MyService
I inject services in forEach block like this
beforeEach(angular.mock.inject(function(_MyService_) {
I wonder if its because I am not using the fake service?
describe('MyCtrl', function() {
//Data Exposure Prep
var $controller;
var $rootScope;
var $scope;
var controller;
var MyService;
dd1 = {
itinerary: globalMockData.d1,//I stored globalMockData somewhere else
};
beforeEach(angular.mock.module('myapp'));
beforeEach(angular.mock.inject(function(_$rootScope_, _$controller_, _$httpBackend_) {
$rootScope = _$rootScope_;
$controller = _$controller_;
$httpBackend = _$httpBackend_;
$scope = $rootScope.$new();
controller = $controller('MyCtrl', { $scope: $scope }, dd1);
}));
//BASIC INFO
describe('should receive sth', function() {
it('finds sth', function() {
expect(controller.answer).toBeDefined();
});
});
});
It really depend on your Controlller implementation.
If you're using scope, you'll probably want to test things on controller's scope, not on the controller itself.
Testing controller with $scope
function MyController($scope, MyService) {
$scope.greetUpperCase = function(name) {
return MyService.greet(name).toUpperCase();
}
}
Basically, when you’re working with $scope, you don’t really care about the controller itself, since it’s doing everything on it’s scope.
So, we will need to:
inject $rootScope
create a new scope
inject it to a new controller
test the $scope
it('test greetUpperCase()', function() {
var myScope = $rootScope.$new();
$controller('MyController', {
$scope: myScope
});
expect(myScope .greetUpperCase('bob')).toBe('HELLO BOB');
});
https://jsfiddle.net/ronapelbaum/pkhaxmdg/

Declaring functions and properties in the scope of angularjs controller but not attached to $scope

As of recently I have been declaring functions and properties for my angularJS controllers in the following way (app is set to the main apps angular module):
app.controller('myController', ['$scope', function($scope) {
$scope.myProperty = "hello world";
$scope.myFunc = function() {
// do stuff
};
}]);
After a while the controllers' $scope grew to contain many utility functions and properties that are not being used directly in my views and would not be applicable to other controllers, so I changed it to this:
app.controller('myController', ['$scope', function($scope) {
var myProperty = 0, addOne;
addOne = function(i) {
return i++;
};
$scope.myFunc = function() {
myProperty = addOne(myProperty);
// do other stuff
};
}]);
This is working fine but is it okay to declare functions and properties the way shown above or should I extract them out into a service? Can I unit test var declared functions in my controller from jasmine/karma(tried but did not succeed)? What is the best pattern to follow?
I have looked into using the 'this' keyword to accomplish the same thing shown above but I don't think the added value would outweigh the amount of time it would take to convert everything to this pattern. If this is totally wrong please advise a better strategy.
--- Update ---
To answer my own question about testing var declared functions in the controller: How can we test non-scope angular controller methods?
As a general rule, if you're not going to use the variable or function in the view, don't attach it to the scope. So declare your variables and functions within the controller or on the scope based on it's use. You don't want to put unnecessary stuff on the $scope, as this will slow down the digest cycle. As a best practise I follow this format in my controller:
app.controller('MainCtrl', function($scope) {
// controller variables
var age = 123;
// scope variables
$scope.name = 'John';
// controller functions
var helloWorld = function() {
console.log('do work');
}
// scope functions
$scope.isOld = function() {
return age > 50;
}
});
services are singletons while controllers are not.
in addition, services are used to share functionality between many different controllers/directives.
your second code block looks really good to me for internal, controller specific functionality which you don't need to share in other places in your app, and its much better than putting unneeded stuff in the scope.

check if there are any directives on a controller

I would like to build a service that could check if there were any directives in use on a controller and if so build a directives object and populate it with objects for each directive on the controller. The benefit of this would be that I could build a type of API for scope inheritance automatically for all of my directives. Right now I'm essentially doing the same thing but by hand. Is there some property on the scope object that I can use to find the names of directives in use? Here's an example of what I mean
app.controller('controller1' ['$scope', function($scope) {
$scope.directivesAPI = {};
$scope.directivesAPI.privateScope = {};
}]);
app.directive('privateScope', function() {
restrict: A,
scope: true,
link: function(scope, elem, attr) {
scope.directivesAPI.privateScope.what = "My Job is to provide new features";
scope.directivesAPI.privateScope.how = "I share the scope using this fancy API";
}
})
<div ng-controller="controller1" privateScope>
{{directivesAPI.what}}
{{directivesAPI.how}}
</div>
In this way I can share the scope without polluting the parent scope. So my question is, rather than having to build the directivesAPI by hand I'd like to be able to do something more like this
app.controller('controller1' ['$scope', 'directivesAPI' function($scope,directivesAPI) {
$scope.directivesAPI = directivesAPI.build();
//The service is responsible for finding the directives and building the object for me.
}]);
If anyone has any ideas I'd love to hear them.
So, from what I understand, what you want is to be able to have a common "scope" that you can use between all of your directives?
If that's the case, then what you could do instead is simply create a service. You could use the service across all of your directives, and treat it as if it were a private "scope". Something like what I have below:
var myApp = angular.module('myApp', []);
myApp.controller('GreetingController', ['$scope', 'myPrivateScope',
function($scope, myPrivateScope) {
$scope.greeting = myPrivateScope.greeting;
}
]);
myApp.service('myPrivateScope', function () {
var srv = {};
srv.greeting = 'Hola!';
return srv;
});
You can use $injector to get the service in your directive. See the answer to AngularJS. Passing Service as an argument to Directive.

Angular controller not initialized in jasmine test

I ran into issues writing jasmine tests for an AngularJS application using angular ui-router. My services and app get initialized properly in the test, but the controllers do not start up properly. I've taken the application in question out of the equation and reduced the problem to a simple one controller example that exhibits the same behavior. Here's the actual test code:
describe('Test', function() {
var async = new AsyncSpec(this);
var scope = {};
beforeEach(angular.mock.module('TestApp'));
beforeEach(angular.mock.inject(function($rootScope, $state, $templateCache) {
scope.$rootScope = $rootScope;
scope.$state = $state;
$templateCache.put('start.html', '<div class="start"></div>');
}));
async.it('Test that TestCtrl is initialized', function(done) {
scope.$rootScope.status = { done: false };
scope.$rootScope.$on('$stateChangeSuccess', function(event, state, params) {
expect(scope.$rootScope.status.done).toBe(true);
done();
});
scope.$state.transitionTo('start', {}, { notify: true });
scope.$rootScope.$apply();
});
});
Here's the complete runnable test
The application gets initialized correctly, the ui router is able to transition the application to the correct state, but the controller does not get initialized. I need the router to initialize the controllers as the router passes critical configuration to them. I want to avoid duplicating that configuration in the tests.
I must be missing something, but what? I appreciate any and all input, thanks!
You need to use the $controller service to instantiate your controller in your tests and pass it your scope. For example...
ctrl = $controller('TestCtrl', {$scope: scope});
Notice that I also moved the declaration of $rootScope.done to the TestCtrl to prevent an error about $rootScope.done being undefined. Here's the fiddle...
http://jsfiddle.net/C8QtB/3/

Categories

Resources