Angular test module controllers without a global controller

In the excellent Vojta Jina repository, where he demonstrates directive testing, he defines a directory controller outside the module shell. Take a look here: https://github.com/vojtajina/ng-directive-testing/blob/master/js/tabs.js

Isn't that bad practice and polluting the global namespace?

If someone had a different place where it would be logical to call something TabsController, isn't this a break?

Tests for the specified directive can be found here: https://github.com/vojtajina/ng-directive-testing/commit/test-controller

Can directive controllers be tested separately from the rest of the directive without putting the controller in the global namespace?

It would be nice to encapsulate the entire directive in the definition of app.directive (...).

+63
angularjs unit-testing angularjs-directive karma-runner jasmine
Mar 09 '13 at 18:24
source share
5 answers

Great question!

So, this is a common problem not only with the controllers, but also potentially with the services that the directive may require in order to carry out its work, but do not necessarily want to expose this controller / service to the “outside world”.

I strongly believe that global data is evil and should be avoided, and this also applies to directory controllers . If we accept this assumption, we can use several different approaches to define these controllers “locally”. In this case, we need to keep in mind that the controller should be "easily" available for unit tests , so we can not just hide it in a directory closure. IMO features:

1) First, we could just define a directory controller at the module level , ex ::

angular.module('ui.bootstrap.tabs', []) .controller('TabsController', ['$scope', '$element', function($scope, $element) { ... }]) .directive('tabs', function() { return { restrict: 'EA', transclude: true, scope: {}, controller: 'TabsController', templateUrl: 'template/tabs/tabs.html', replace: true }; }) 

This is a simple method that we use at https://github.com/angular-ui/bootstrap/blob/master/src/tabs/tabs.js , which is based on the work of Vojta.

Although this is a very simple method, it should be noted that the controller is still exposed to the entire application, which means that another module can potentially override it. In this sense, the controller localizes the AngularJS application (therefore, it does not pollute the global area of ​​the window), but also globally for all AngularJS modules.

2) Use the closing area and special files for testing .

If we want to completely hide the controller function, we can wrap the code in closure. This is the method that uses AngularJS. For example, looking at the NgModelController , we see that it is defined as a “global” function in its own files (and, therefore, is easily accessible for testing), but the entire file is closed during the build time:

To summarize: option (2) is “more secure”, but a bit of pre-configuration is required for assembly.

+57
Mar 09 '13 at 19:22
source share

I prefer to turn on my controller from time to time along with the directive, so I need a way to check this.

Directive first

 angular.module('myApp', []) .directive('myDirective', function() { return { restrict: 'EA', scope: {}, controller: function ($scope) { $scope.isInitialized = true }, template: '<div>{{isInitialized}}</div>' } }) 

Then tests:

 describe("myDirective", function() { var el, scope, controller; beforeEach inject(function($compile, $rootScope) { # Instantiate directive. # gotacha: Controller and link functions will execute. el = angular.element("<my-directive></my-directive>") $compile(el)($rootScope.$new()) $rootScope.$digest() # Grab controller instance controller = el.controller("myDirective") # Grab scope. Depends on type of scope. # See angular.element documentation. scope = el.isolateScope() || el.scope() }) it("should do something to the scope", function() { expect(scope.isInitialized).toBeDefined() }) }) 

See angular documentation for more ways to get data from the directive you created.

Beware that instantiating the directive implies that the controller and all link functions are already running, which may affect your tests.

+69
Jul 15 '14 at 14:41
source share

James's method works for me. One small twist though, if you have an external template, you will need to call $ httpBackend.flush () before $ rootScope. $ Digest () to let angular execute your controller.

I think this should not be a problem if you use https://github.com/karma-runner/karma-ng-html2js-preprocessor

+9
Feb 18 '15 at 7:28
source share

Is there something wrong with this? Seems preferable since you avoid placing your controller in the global namespace and can test what you want (i.e. the Controller) without having to compile html $.

An example of a directive definition:

  .directive('tabs', function() { return { restrict: 'EA', transclude: true, scope: {}, controller: function($scope, $attrs) { this.someExposedMethod = function() {}; }, templateUrl: 'template/tabs/tabs.html', replace: true }; 

Then in your Jasmine test, ask for the directive you created using "name + Directive" (for example, "tabsDirective"):

 var tabsDirective = $injector.get('tabsDirective')[0]; // instantiate and override locals with mocked test data var tabsDirectiveController = $injector.instantiate(tabsDirective.controller, { $scope: {...} $attrs: {...} }); 

Now you can test the controller methods:

 expect(typeof tabsDirectiveController.someExposedMethod).toBe('function'); 
+3
Aug 05 '16 at 11:39 on
source share

Use IIFE, which is a common technique that avoids the conflict of the global namespace, as well as save complex built-in gymnastics, as well as provide freedom in its volume.

  (function(){ angular.module('app').directive('myDirective', function(){ return { ............. controller : MyDirectiveController, ............. } }); MyDirectiveController.$inject = ['$scope']; function MyDirectiveController ($scope) { } })(); 
0
Dec 08 '17 at 15:47
source share



All Articles