Unit Testing AngularJS-Direktive mit templateUrl

122

Ich habe eine AngularJS-Direktive, die eine templateUrldefinierte hat. Ich versuche es mit Jasmine zu testen.

Mein Jasmine JavaScript sieht wie folgt aus , gemäß der Empfehlung von dieser :

describe('module: my.module', function () {
    beforeEach(module('my.module'));

    describe('my-directive directive', function () {
        var scope, $compile;
        beforeEach(inject(function (_$rootScope_, _$compile_, $injector) {
            scope = _$rootScope_;
            $compile = _$compile_;
            $httpBackend = $injector.get('$httpBackend');
            $httpBackend.whenGET('path/to/template.html').passThrough();
        }));

        describe('test', function () {
            var element;
            beforeEach(function () {
                element = $compile(
                    '<my-directive></my-directive>')(scope);
                angular.element(document.body).append(element);
            });

            afterEach(function () {
                element.remove();
            });

            it('test', function () {
                expect(element.html()).toBe('asdf');
            });

        });
    });
});

Wenn ich dies in meinem Jasmine-Spezifikationsfehler ausführe, wird der folgende Fehler angezeigt:

TypeError: Object #<Object> has no method 'passThrough'

Ich möchte nur, dass die templateUrl so geladen wird, wie sie ist - ich möchte sie nicht verwenden respond. Ich glaube, dies hängt möglicherweise damit zusammen, dass ngMock anstelle von ngMockE2E verwendet wird . Wenn dies der Schuldige ist, wie verwende ich letzteres anstelle des ersteren?

Danke im Voraus!

Jared
quelle
1
Ich habe es nicht .passThrough();auf diese Weise verwendet, aber aus den Dokumenten haben Sie Folgendes versucht: $httpBackend.expectGET('path/to/template.html'); // do action here $httpBackend.flush();Ich denke, dies passt besser zu Ihrer Verwendung - Sie möchten die Anfrage nicht abfangen, dh whenGet()stattdessen überprüfen, ob sie gesendet wurde, und dann tatsächlich schicken Sie es?
Alex Osborn
1
Danke für die Antwort. Ich glaube nicht, dass expectGETdas Anfragen sendet ... zumindest sofort. In den Dokumenten hat ihr Beispiel mit /auth.pyein $httpBackend.whenvor dem $httpBackend.expectGETund $httpBackend.flushruft auf.
Jared
2
Das ist richtig, es expectGetwird nur geprüft, ob eine Anfrage versucht wurde.
Alex Osborn
1
Ah. Nun, ich brauche eine Möglichkeit, dem $httpBackendMock zu sagen , dass er die in der Direktive unter angegebene URL tatsächlich verwenden templateUrlund sie abrufen soll. Ich dachte, ich passThroughwürde das tun. Kennen Sie einen anderen Weg, dies zu tun?
Jared
2
Hmm, ich habe nicht viel e2e noch Tests durchgeführt, aber die Dokumentation überprüft - haben Sie die e2e - Backend versucht , mit statt - ich denke , das ist , warum Sie keine Methode bekam passthrough - docs.angularjs.org/api/ngMockE2E.$httpBackend
Alex Osborn

Antworten:

187

Sie haben Recht, dass es mit ngMock zusammenhängt. Das ngMock-Modul wird automatisch für jeden Angular-Test geladen und initialisiert den Mock $httpBackend, um jede Verwendung des $httpDienstes zu handhaben , einschließlich des Abrufs von Vorlagen. Das Vorlagensystem versucht, die Vorlage durchzuladen, $httpund es wird eine "unerwartete Anforderung" an den Mock.

Was Sie brauchen, um die Vorlagen vorab in das zu laden, $templateCachedamit sie bereits verfügbar sind, wenn Angular sie anfordert, ohne sie zu verwenden $http.

Die bevorzugte Lösung: Karma

Wenn Sie Karma zum Ausführen Ihrer Tests verwenden (und dies sollten Sie auch tun ), können Sie es so konfigurieren, dass die Vorlagen mit dem Präprozessor ng-html2js für Sie geladen werden . Ng-html2js liest die von Ihnen angegebenen HTML-Dateien und konvertiert sie in ein Angular-Modul, das die vorinstalliert $templateCache.

Schritt 1: Aktivieren und konfigurieren Sie den Präprozessor in Ihrem karma.conf.js

// karma.conf.js

preprocessors: {
    "path/to/templates/**/*.html": ["ng-html2js"]
},

ngHtml2JsPreprocessor: {
    // If your build process changes the path to your templates,
    // use stripPrefix and prependPrefix to adjust it.
    stripPrefix: "source/path/to/templates/.*/",
    prependPrefix: "web/path/to/templates/",

    // the name of the Angular module to create
    moduleName: "my.templates"
},

Wenn Sie Yeoman als Gerüst für Ihre App verwenden, funktioniert diese Konfiguration

plugins: [ 
  'karma-phantomjs-launcher', 
  'karma-jasmine', 
  'karma-ng-html2js-preprocessor' 
], 

preprocessors: { 
  'app/views/*.html': ['ng-html2js'] 
}, 

ngHtml2JsPreprocessor: { 
  stripPrefix: 'app/', 
  moduleName: 'my.templates' 
},

Schritt 2: Verwenden Sie das Modul in Ihren Tests

// my-test.js

beforeEach(module("my.templates"));    // load new module containing templates

Ein vollständiges Beispiel finden Sie in diesem kanonischen Beispiel des Angular-Test-Gurus Vojta Jina . Es enthält ein komplettes Setup: Karma-Konfiguration, Vorlagen und Tests.

Eine Nicht-Karma-Lösung

Wenn Sie Karma aus irgendeinem Grund nicht verwenden (ich hatte einen unflexiblen Erstellungsprozess in der Legacy-App) und nur in einem Browser testen, habe ich festgestellt, dass Sie die Übernahme von ngMock umgehen können, indem Sie $httpBackenddie Vorlage mithilfe eines rohen XHR abrufen und legen Sie es in die $templateCache. Diese Lösung ist viel weniger flexibel, erledigt aber die Aufgabe vorerst.

// my-test.js

// Make template available to unit tests without Karma
//
// Disclaimer: Not using Karma may result in bad karma.
beforeEach(inject(function($templateCache) {
    var directiveTemplate = null;
    var req = new XMLHttpRequest();
    req.onload = function() {
        directiveTemplate = this.responseText;
    };
    // Note that the relative path may be different from your unit test HTML file.
    // Using `false` as the third parameter to open() makes the operation synchronous.
    // Gentle reminder that boolean parameters are not the best API choice.
    req.open("get", "../../partials/directiveTemplate.html", false);
    req.send();
    $templateCache.put("partials/directiveTemplate.html", directiveTemplate);
}));

Aber im Ernst. Benutze Karma . Das Einrichten erfordert ein wenig Arbeit, aber Sie können alle Ihre Tests in mehreren Browsern gleichzeitig über die Befehlszeile ausführen. Sie können es also als Teil Ihres kontinuierlichen Integrationssystems verwenden und / oder es zu einer Tastenkombination in Ihrem Editor machen. Viel besser als Alt-Tab-Refresh-Ad-Infinitum.

SleepyMurph
quelle
6
Dies mag offensichtlich sein, aber wenn andere bei der gleichen Sache stecken bleiben und hier nach Antworten suchen: Ich könnte es nicht zum Laufen bringen, ohne auch das preprocessorsDateimuster (z. B. "path/to/templates/**/*.html") zum filesAbschnitt in hinzuzufügen karma.conf.js.
Johan
1
Gibt es also größere Probleme, wenn Sie nicht auf die Antwort warten, bevor Sie fortfahren? Wird der Wert nur aktualisiert, wenn die Anforderung zurückkommt (IE dauert 30 Sekunden)?
Jackie
1
@Jackie Ich gehe davon aus, dass Sie über das Beispiel "Nicht-Karma" sprechen, bei dem ich den falseParameter für den openAufruf des XHR verwende , um ihn synchron zu machen. Wenn Sie dies nicht tun, wird die Ausführung fröhlich fortgesetzt und die Ausführung Ihrer Tests gestartet, ohne dass die Vorlage geladen wird. Damit haben Sie wieder das gleiche Problem: 1) Die Anforderung einer Vorlage geht aus. 2) Der Test wird ausgeführt. 3) Der Test kompiliert eine Direktive und die Vorlage wird noch nicht geladen. 4) Angular fordert die Vorlage über seinen $httpverspotteten Dienst an. 5) Der Mock- $httpService beschwert sich: "unerwartete Anfrage".
SleepyMurph
1
Ich konnte Grunz-Jasmin ohne Karma laufen lassen.
FlavorScape
5
Eine andere Sache: Sie müssen karma-ng-html2js-preprocessor ( npm install --save-dev karma-ng-html2js-preprocessor) installieren und es karma.conf.jsgemäß stackoverflow.com/a/19077966/859631 zum Plugins-Bereich Ihres hinzufügen .
Vincent
37

Am Ende habe ich den Vorlagen-Cache abgerufen und die Ansicht dort abgelegt. Ich habe keine Kontrolle darüber, ob ich ngMock nicht benutze. Es stellt sich heraus:

beforeEach(inject(function(_$rootScope_, _$compile_, $templateCache) {
    $scope = _$rootScope_;
    $compile = _$compile_;
    $templateCache.put('path/to/template.html', '<div>Here goes the template</div>');
}));
Jared
quelle
26
Hier ist meine Beschwerde mit dieser Methode ... Wenn wir nun ein großes Stück HTML haben, das wir als Zeichenfolge in den Vorlagen-Cache einfügen, was werden wir dann tun, wenn wir das HTML am Frontend ändern ? Ändern Sie den HTML-Code auch im Test? IMO, das ist eine nicht nachhaltige Antwort und der Grund, warum wir die Option template over templateUrl verwendet haben. Obwohl ich es sehr ablehne, mein HTML als massiven String in der Direktive zu haben, ist es die nachhaltigste Lösung, nicht zwei HTML-Stellen aktualisieren zu müssen. Was nicht viel Bildgebung erfordert, dass das HTML im Laufe der Zeit nicht übereinstimmen kann.
Sten Muchow
12

Dieses anfängliche Problem kann gelöst werden, indem Folgendes hinzugefügt wird:

beforeEach(angular.mock.module('ngMockE2E'));

Dies liegt daran, dass standardmäßig versucht wird, $ httpBackend im ngMock- Modul zu finden, und dass es nicht voll ist.

Bullgare
quelle
1
Nun, das ist in der Tat die richtige Antwort auf die ursprüngliche Frage (die mir geholfen hat).
Mat
Versuchte dies, aber passThrough () funktionierte immer noch nicht für mich. Es gab immer noch den Fehler "Unerwartete Anfrage".
frodo2975
8

Die Lösung, die ich erreicht habe, benötigt jasmine-jquery.js und einen Proxyserver.

Ich habe diese Schritte befolgt:

  1. In der karma.conf:

Fügen Sie jasmine-jquery.js zu Ihren Dateien hinzu

files = [
    JASMINE,
    JASMINE_ADAPTER,
    ...,
    jasmine-jquery-1.3.1,
    ...
]

Fügen Sie einen Proxyserver hinzu, der Ihre Geräte verwaltet

proxies = {
    '/' : 'http://localhost:3502/'
};
  1. In Ihrer Spezifikation

    beschreiben ('MySpec', function () {var $ scope, template; jasmine.getFixtures (). fixturesPath = 'public / partials /'; // benutzerdefinierter Pfad, damit Sie die reale Vorlage bereitstellen können, die Sie in der App vor jeder Funktion verwenden () {template = angle.element ('');

        module('project');
        inject(function($injector, $controller, $rootScope, $compile, $templateCache) {
            $templateCache.put('partials/resources-list.html', jasmine.getFixtures().getFixtureHtml_('resources-list.html')); //loadFixture function doesn't return a string
            $scope = $rootScope.$new();
            $compile(template)($scope);
            $scope.$apply();
        })
    });

    });

  2. Führen Sie einen Server im Stammverzeichnis Ihrer App aus

    Python -m SimpleHTTPServer 3502

  3. Führen Sie Karma.

Ich habe eine Weile gebraucht, um das herauszufinden, da ich viele Beiträge durchsuchen musste. Ich denke, die Dokumentation dazu sollte klarer sein, da es sich um ein so wichtiges Thema handelt.

Tomas Romero
quelle
Ich hatte Probleme beim Bereitstellen von Assets localhost/base/specsund beim Hinzufügen eines Proxyservers, auf dem das Problem python -m SimpleHTTPServer 3502behoben wurde. Sie, mein Herr, sind ein Genie!
Pbojinov
In meinen Tests wurde ein leeres Element von $ compile zurückgegeben. An anderen Stellen wurde vorgeschlagen, $ scope auszuführen. $ Digest (): noch leer. Das Ausführen von $ scope. $ Apply () hat jedoch funktioniert. Ich denke, das lag daran, dass ich in meiner Direktive einen Controller verwende? Nicht sicher. Danke für den Hinweis! Geholfen!
Sam Simmons
7

Meine Lösung:

test/karma-utils.js::

function httpGetSync(filePath) {
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/base/app/" + filePath, false);
  xhr.send();
  return xhr.responseText;
}

function preloadTemplate(path) {
  return inject(function ($templateCache) {
    var response = httpGetSync(path);
    $templateCache.put(path, response);
  });
}

karma.config.js::

files: [
  //(...)
  'test/karma-utils.js',
  'test/mock/**/*.js',
  'test/spec/**/*.js'
],

der Test:

'use strict';
describe('Directive: gowiliEvent', function () {
  // load the directive's module
  beforeEach(module('frontendSrcApp'));
  var element,
    scope;
  beforeEach(preloadTemplate('views/directives/event.html'));
  beforeEach(inject(function ($rootScope) {
    scope = $rootScope.$new();
  }));
  it('should exist', inject(function ($compile) {
    element = angular.element('<event></-event>');
    element = $compile(element)(scope);
    scope.$digest();
    expect(element.html()).toContain('div');
  }));
});
Bartek
quelle
Erste anständige Lösung, die Entwickler nicht dazu zwingt, Karma zu verwenden. Warum sollten eckige Jungs etwas so Schlimmes und leicht Vermeidbares inmitten von etwas so Coolem tun? pfff
Fabio Milheiro
Ich sehe, dass Sie ein 'test / mock / ** / *. Js' hinzufügen und ich nehme an, es ist, alle verspotteten Sachen wie Dienste und alles zu laden? Ich suche nach Möglichkeiten, um Codeduplizierungen von verspotteten Diensten zu vermeiden. Würden Sie uns dazu etwas mehr zeigen?
Stephane
Ich erinnere mich nicht genau, aber es gab wahrscheinlich Einstellungen, zum Beispiel JSONs für den $ http-Dienst. Nichts Besonderes.
Bartek
Hatte dieses Problem heute - tolle Lösung. Wir verwenden Karma, aber wir verwenden auch Chutzpah - kein Grund, warum wir gezwungen sein sollten, Karma zu verwenden, und nur Karma, um Anweisungen für Unit-Tests durchführen zu können.
Lwalden
Wir verwenden Django mit Angular, und dies hat wie ein Zauber funktioniert, um eine Direktive zu testen, die ihre templateUrl lädt static, z. B. beforeEach(preloadTemplate(static_url +'seed/partials/beChartDropdown.html')); Danke!
Aleck Landgraf
6

Wenn Sie Grunt verwenden, können Sie Grunt-Angular-Templates verwenden. Es lädt Ihre Vorlagen in den templateCache und ist unabhängig von Ihrer Spezifikationskonfiguration.

Meine Beispielkonfiguration:

module.exports = function(grunt) {

  grunt.initConfig({

    pkg: grunt.file.readJSON('package.json'),

    ngtemplates: {
        myapp: {
          options: {
            base:       'public/partials',
            prepend:    'partials/',
            module:     'project'
          },
          src:          'public/partials/*.html',
          dest:         'spec/javascripts/angular/helpers/templates.js'
        }
    },

    watch: {
        templates: {
            files: ['public/partials/*.html'],
            tasks: ['ngtemplates']
        }
    }

  });

  grunt.loadNpmTasks('grunt-angular-templates');
  grunt.loadNpmTasks('grunt-contrib-watch');

};
Tomas Romero
quelle
6

Ich habe das gleiche Problem auf eine etwas andere Weise gelöst als die gewählte Lösung.

  1. Zuerst habe ich das ng-html2js Plugin für Karma installiert und konfiguriert . In der Datei karma.conf.js:

    preprocessors: {
      'path/to/templates/**/*.html': 'ng-html2js'
    },
    ngHtml2JsPreprocessor: {
    // you might need to strip the main directory prefix in the URL request
      stripPrefix: 'path/'
    }
  2. Dann habe ich das in beforeEach erstellte Modul geladen. In Ihrer Spec.js-Datei:

    beforeEach(module('myApp', 'to/templates/myTemplate.html'));
  3. Dann habe ich $ templateCache.get verwendet, um es in einer Variablen zu speichern. In Ihrer Spec.js-Datei:

    var element,
        $scope,
        template;
    
    beforeEach(inject(function($rootScope, $compile, $templateCache) {
      $scope = $rootScope.$new();
      element = $compile('<div my-directive></div>')($scope);
      template = $templateCache.get('to/templates/myTemplate.html');
      $scope.$digest();
    }));
  4. Schließlich habe ich es so getestet. In Ihrer Spec.js-Datei:

    describe('element', function() {
      it('should contain the template', function() {
        expect(element.html()).toMatch(template);
      });
    });
glepretre
quelle
4

Um die Vorlage HTML dynamisch in $ templateCache zu laden, können Sie einfach den Karma-Vorprozessor html2js verwenden, wie hier erläutert

Dies läuft darauf hinaus, Ihren Dateien in der Datei conf.js Vorlagen ' .html' sowie Präprozessoren hinzuzufügen = {' .html': 'html2js'};

und verwenden

beforeEach(module('..'));

beforeEach(module('...html', '...html'));

in Ihre js Testdatei

Lior
quelle
Ich bekommeUncaught SyntaxError: Unexpected token <
Melbourne2991
2

Wenn Sie Karma verwenden, sollten Sie karma-ng-html2js-preprocessor verwenden , um Ihre externen HTML-Vorlagen vorkompilieren zu können, und vermeiden, dass Angular versucht, sie während der Testausführung per HTTP abzurufen . Ich hatte einige Probleme damit - in meinem Fall wurden die Teilpfade von templateUrl während der normalen App-Ausführung, aber nicht während der Tests aufgelöst - aufgrund von Unterschieden in den Strukturen von App und Testverzeichnis.

Nikita
quelle
2

Wenn Sie das Jasmin-Maven-Plugin zusammen mit RequireJS verwenden, können Sie das Text-Plugin verwenden , um den Vorlageninhalt in eine Variable zu laden und ihn dann in den Vorlagen-Cache zu legen.


define(['angular', 'text!path/to/template.html', 'angular-route', 'angular-mocks'], function(ng, directiveTemplate) {
    "use strict";

    describe('Directive TestSuite', function () {

        beforeEach(inject(function( $templateCache) {
            $templateCache.put("path/to/template.html", directiveTemplate);
        }));

    });
});
Leonard Brünings
quelle
Kannst du das ohne Karma machen?
Winnemucca
2

Wenn Sie in Ihren Tests requirejs verwenden, können Sie das 'text'-Plugin verwenden, um die HTML-Vorlage abzurufen und in den $ templateCache einzufügen.

require(["text!template.html", "module-file"], function (templateHtml){
  describe("Thing", function () {

    var element, scope;

    beforeEach(module('module'));

    beforeEach(inject(function($templateCache, $rootScope, $compile){

      // VOILA!
      $templateCache.put('/path/to/the/template.html', templateHtml);  

      element = angular.element('<my-thing></my-thing>');
      scope = $rootScope;
      $compile(element)(scope);   

      scope.$digest();
    }));
  });
});
Tim Kindberg
quelle
0

Ich behebe dieses Problem beim Kompilieren aller Vorlagen in den Vorlagencache. Ich benutze Schluck, Sie können ähnliche Lösung auch für Grunzen finden. Meine templateUrls in Direktiven, Modalitäten sehen aus wie

`templateUrl: '/templates/directives/sidebar/tree.html'`
  1. Fügen Sie ein neues npm-Paket in meiner package.json hinzu

    "gulp-angular-templatecache": "1.*"

  2. Fügen Sie in der Gulp-Datei den Vorlagencache und eine neue Aufgabe hinzu:

    var templateCache = require('gulp-angular-templatecache'); ... ... gulp.task('compileTemplates', function () { gulp.src([ './app/templates/**/*.html' ]).pipe(templateCache('templates.js', { transformUrl: function (url) { return '/templates/' + url; } })) .pipe(gulp.dest('wwwroot/assets/js')); });

  3. Fügen Sie alle js-Dateien in index.html hinzu

    <script src="/assets/js/lib.js"></script> <script src="/assets/js/app.js"></script> <script src="/assets/js/templates.js"></script>

  4. Genießen!

Kitolog
quelle