Unit-testing directive controllers in Angular without making controller global

Unit TestingAngularjsAngularjs DirectiveJasmineKarma Runner

Unit Testing Problem Overview


In Vojta Jina's excellent repository where he demonstrates testing of directives, he defines the directive controller outside of the module wrapper. See here: https://github.com/vojtajina/ng-directive-testing/blob/master/js/tabs.js

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

If one were to have another place where it might be logical to call something TabsController, wouldn't that break stuff?

The tests for the mentioned directive is to be found here: https://github.com/vojtajina/ng-directive-testing/commit/test-controller

Is it possible to test directive controllers separate from the rest of the directive, without placing the controller in a global namespace?

It would be nice to encapsulate the whole directive within the app.directive(...) definition.

Unit Testing Solutions


Solution 1 - Unit Testing

I prefer at times to include my controller along with the directive so I need a way to test that.

First the directive

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

Then the 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.element documentation for more ways to get data from an instantiated directive.

Beware that instantiating the directive implies that the controller and all link functions will already have run, so that might affect your tests.

Solution 2 - Unit Testing

Excellent question!

So, this is a common concern, not only with controllers but also potentially with services that a directive might need to perform its job but don't necessarily want to expose this controller / service to the "external world".

I strongly believe that global data are evil and should be avoided and this applies to directive controllers as well. If we take this assumption we can take several different approaches to define those controllers "locally". While doing so we need to keep in mind that a controller should be still "easily" accessible to unit tests so we can't simply hide it into directive's closure. IMO possibilities are:

  1. Firstly, we could simply define directive's controller on a 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 technique that we are using in https://github.com/angular-ui/bootstrap/blob/master/src/tabs/tabs.js which is based on Vojta's work.

While this is a very simple technique it should be noted that a controller is still exposed to the whole application which means that other module could potentially override it. In this sense it makes a controller local to AngularJS application (so not polluting a global window scope) but it also global to all AngularJS modules.

  1. Use a closure scope and special files setup for testing.

If we want to completely hide a controller function we can wrap code in a closure. This is a technique that AngularJS is using. For example, looking at the [NgModelController][1] we can see that it is defined as a "global" function in its own files (and thus easily accessible for testing) but the whole file is wrapped in closure during the build time:

To sum up: the option (2) is "safer" but requires a bit of up-front setup for the build. [1]: https://github.com/angular/angular.js/blob/master/src/ng/directive/input.js#L891

Solution 3 - Unit Testing

James's method works for me. One small twist is though, when you have an external template, you would have to call $httpBackend.flush() before $rootScope.$digest() in order to let angular execute your controller.

I guess this should not be an issue, if you are using https://github.com/karma-runner/karma-ng-html2js-preprocessor

Solution 4 - Unit Testing

Is there something wrong with doing it this way? Seems preferable since you avoid placing your controller in the global name space and are able to test what you want (i.e. the controller) without unnecessarily $compiling html.

Example 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" (ex. "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 controller methods:

expect(typeof tabsDirectiveController.someExposedMethod).toBe('function');

Solution 5 - Unit Testing

Use IIFE, which is a common technique to avoid global namespace conflict & it also save tricky inline gymnastics, plus provide freedom in your scope.

 (function(){

  angular.module('app').directive('myDirective', function(){
     return {
       .............
       controller : MyDirectiveController,
       .............
     }
  });

  MyDirectiveController.$inject = ['$scope'];

  function MyDirectiveController ($scope) {
    
  }

})();

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionKenneth LynneView Question on Stackoverflow
Solution 1 - Unit TestingJames van DykeView Answer on Stackoverflow
Solution 2 - Unit Testingpkozlowski.opensourceView Answer on Stackoverflow
Solution 3 - Unit Testingbuddyspike28View Answer on Stackoverflow
Solution 4 - Unit TestingjbmilgromView Answer on Stackoverflow
Solution 5 - Unit TestingImdadul Huq NaimView Answer on Stackoverflow