webentwicklung-frage-antwort-db.com.de

Unit-Test-Direktiven-Controller in Angular, ohne Controller global zu machen

In Vojta Jinas exzellentem Repository, in dem er das Testen von Direktiven demonstriert, definiert er den Direktiven-Controller außerhalb des Modul-Wrappers. Siehe hier: https://github.com/vojtajina/ng-directive-testing/blob/master/js/tabs.js

Ist das nicht schlechte Praxis und verschmutzen den globalen Namespace?

Wenn man einen anderen Ort hätte, an dem es logisch wäre, etwas TabsController zu nennen, würde das dann nichts kaputt machen?

Die Tests für die genannte Richtlinie finden Sie hier: https://github.com/vojtajina/ng-directive-testing/commit/test-controller

Ist es möglich, Direktiven-Controller getrennt von den übrigen Direktiven zu testen, ohne den Controller in einen globalen Namespace zu stellen?

Es wäre schön, die gesamte Richtlinie in der Definition von app.directive (...) zu kapseln.

66
Kenneth Lynne

Hervorragende Frage!

Dies ist also ein häufiges Problem, nicht nur bei Controllern, sondern möglicherweise auch bei Diensten, die eine Direktive möglicherweise benötigt, um ihre Aufgabe zu erfüllen, die diese Controller/Dienste aber nicht unbedingt der "Außenwelt" aussetzen möchten.

Ich bin der festen Überzeugung, dass globale Daten böse sind und vermieden werden sollten, und dies gilt auch für Direktiven-Controller . Wenn wir diese Annahme treffen, können wir verschiedene Ansätze verfolgen, um diese Controller "lokal" zu definieren. Dabei müssen wir berücksichtigen, dass ein Controller für Komponententests "leicht" zugänglich sein sollte , damit wir ihn nicht einfach in Direktiven verstecken können Schließung. IMO-Möglichkeiten sind:

1) Erstens könnten wir einfach den Controller der Direktive auf Modulebene definieren , 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
  };
})

Dies ist eine einfache Technik, die wir in https://github.com/angular-ui/bootstrap/blob/master/src/tabs/tabs.js verwenden und die auf Vojtas Arbeit basiert.

Obwohl dies eine sehr einfache Technik ist, sollte beachtet werden, dass ein Controller immer noch der gesamten Anwendung ausgesetzt ist, was bedeutet, dass andere Module ihn möglicherweise außer Kraft setzen könnten. In diesem Sinne wird ein Controller lokal für die AngularJS-Anwendung erstellt (wodurch ein globaler Fensterbereich nicht verschmutzt wird), aber auch global für alle AngularJS-Module.

2) Verwenden Sie zum Testen einen Schließbereich und spezielle Dateieinstellungen .

Wenn wir eine Controller-Funktion vollständig verbergen möchten, können wir Code in einen Closure einbinden. Dies ist eine Technik, die AngularJS verwendet. Wenn wir uns beispielsweise NgModelController ansehen, können wir sehen, dass es als "globale" Funktion in seinen eigenen Dateien definiert ist (und daher zum Testen leicht zugänglich ist), aber die gesamte Datei wird während des Builds geschlossen Zeit:

Zusammenfassend lässt sich sagen, dass die Option (2) "sicherer" ist, jedoch eine gewisse Vorabkonfiguration für den Build erfordert.

58

Manchmal ziehe ich es vor, meinen Controller zusammen mit der Direktive einzuschließen, also brauche ich eine Möglichkeit, das zu testen.

Zuerst die Richtlinie

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

Dann die 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()
  })
})

In angle.element-Dokumentation finden Sie weitere Möglichkeiten zum Abrufen von Daten aus einer instanziierten Direktive.

Beachten Sie, dass das Instanziieren der Direktive impliziert, dass der Controller und alle Verknüpfungsfunktionen bereits ausgeführt wurden, was sich auf Ihre Tests auswirken kann.

75
James van Dyke

James Methode funktioniert für mich. Eine kleine Wendung ist jedoch, dass Sie, wenn Sie eine externe Vorlage haben, $ httpBackend.flush () vor $ rootScope. $ Digest () aufrufen müssen, damit angular Ihren Controller ausführen kann.

Ich denke, dies sollte kein Problem sein, wenn Sie https://github.com/karma-runner/karma-ng-html2js-preprocessor verwenden

9
buddyspike28

Ist etwas falsch daran? Scheint vorzuziehen, da Sie vermeiden, Ihren Controller im globalen Namensraum zu platzieren, und in der Lage sind, zu testen, was Sie wollen (d. H. Den Controller), ohne unnötig HTML zu kompilieren.

Beispiel für eine Richtliniendefinition:

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

Fragen Sie dann in Ihrem Jasmin-Test nach der Direktive, die Sie mit "name + Directive" erstellt haben (z. B. "tabsDirective"):

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

Jetzt können Sie Controllermethoden testen:

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

Verwenden Sie IIFE, eine weit verbreitete Technik, um Konflikte mit globalen Namespaces zu vermeiden, und ersparen Sie sich außerdem kniffliges Inline-Turnen.

 (function(){

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

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

  function MyDirectiveController ($scope) {

  }

})();
0