web-dev-qa-db-fra.com

Contrôleurs de directive de test unitaire dans Angular sans rendre le contrôleur global

Dans l'excellent référentiel de Vojta Jina où il démontre les tests de directives, il définit le contrôleur de directives en dehors de l'encapsuleur de module. Voir ici: https://github.com/vojtajina/ng-directive-testing/blob/master/js/tabs.js

N'est-ce pas une mauvaise pratique et polluer l'espace de noms global?

Si l'on devait avoir un autre endroit où il pourrait être logique d'appeler quelque chose TabsController, cela ne casserait-il pas les choses?

Les tests pour la directive mentionnée se trouvent ici: https://github.com/vojtajina/ng-directive-testing/commit/test-controller

Est-il possible de tester des contrôleurs de directive séparés du reste de la directive, sans placer le contrôleur dans un espace de noms global?

Ce serait bien d'encapsuler toute la directive dans la définition app.directive (...).

66
Kenneth Lynne

Excellente question!

Il s'agit donc d'une préoccupation commune, non seulement avec les contrôleurs, mais aussi potentiellement avec les services dont une directive pourrait avoir besoin pour effectuer son travail, mais ne veut pas nécessairement exposer ce contrôleur/service au "monde extérieur".

Je crois fermement que les données globales sont mauvaises et doivent être évitées et cela s'applique également aux contrôleurs de directive . Si nous prenons cette hypothèse, nous pouvons adopter plusieurs approches différentes pour définir ces contrôleurs "localement". En faisant cela, nous devons garder à l'esprit que un contrôleur doit être toujours "facilement" accessible aux tests unitaires afin que nous ne puissions pas simplement le cacher dans les directives fermeture. Les possibilités de l'OMI sont:

1) Premièrement, nous pourrions simplement définir le contrôleur de la directive au niveau du module , 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
  };
})

Il s'agit d'une technique simple que nous utilisons dans https://github.com/angular-ui/bootstrap/blob/master/src/tabs/tabs.js qui est basée sur le travail de Vojta.

Bien qu'il s'agisse d'une technique très simple, il convient de noter qu'un contrôleur est toujours exposé à l'ensemble de l'application, ce qui signifie qu'un autre module pourrait potentiellement le remplacer. En ce sens, il rend un contrôleur local à l'application AngularJS (donc ne pollue pas une étendue de fenêtre globale) mais il est également global à tous les modules AngularJS.

2) Utilisez une étendue de fermeture et une configuration de fichiers spéciaux pour les tests .

Si nous voulons masquer complètement une fonction de contrôleur, nous pouvons envelopper le code dans une fermeture. Il s'agit d'une technique qu'AngularJS utilise. Par exemple, en regardant le NgModelController nous pouvons voir qu'il est défini comme une fonction "globale" dans ses propres fichiers (et donc facilement accessible pour les tests) mais le fichier entier est enveloppé dans une fermeture pendant la construction temps:

Pour résumer: l'option (2) est "plus sûre" mais nécessite un peu de configuration initiale pour la construction.

58

Je préfère parfois inclure mon contrôleur avec la directive, j'ai donc besoin d'un moyen de tester cela.

D'abord la directive

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

Puis les 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()
  })
})

Voir documentation angular.element pour plus de moyens d'obtenir des données à partir d'une directive instanciée.

Attention, l'instanciation de la directive implique que le contrôleur et toutes les fonctions de liaison auront déjà été exécutées, ce qui pourrait affecter vos tests.

75
James van Dyke

La méthode de James fonctionne pour moi. Cependant, lorsque vous disposez d'un modèle externe, vous devez appeler $ httpBackend.flush () avant $ rootScope. $ Digest () afin de laisser angular exécuter votre contrôleur).

Je suppose que cela ne devrait pas être un problème, si vous utilisez https://github.com/karma-runner/karma-ng-html2js-preprocessor

9
buddyspike28

Y a-t-il quelque chose de mal à le faire de cette façon? Cela semble préférable car vous évitez de placer votre contrôleur dans l'espace de nom global et vous pouvez tester ce que vous voulez (c'est-à-dire le contrôleur) sans $ compiler inutilement html.

Exemple de définition de directive:

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

Ensuite, dans votre test Jasmine, demandez la directive que vous avez créée en utilisant "nom + 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: {...}
});

Vous pouvez maintenant tester les méthodes du contrôleur:

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

Utilisez l'IIFF, qui est une technique courante pour éviter les conflits d'espace de noms mondiaux et permet également d'économiser de la gymnastique en ligne délicate, tout en offrant une liberté dans votre champ d'application.

 (function(){

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

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

  function MyDirectiveController ($scope) {

  }

})();
0
Imdadul Huq Naim