Testowanie jednostkowe Dyrektywa AngularJS z templateUrl

122

Mam dyrektywę AngularJS, która ma templateUrlzdefiniowany plik . Próbuję przetestować to z Jasmine.

Moja Jasmine JavaScript wygląda jak poniżej, na zalecenie to :

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');
            });

        });
    });
});

Kiedy uruchamiam to w moim błędzie specyfikacji Jasmine, pojawia się następujący błąd:

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

Wszystko, czego chcę, to załadowanie templateUrl bez zmian - nie chcę go używać respond. Myślę, że może to być związane z używaniem ngMock zamiast ngMockE2E . Jeśli to jest winowajca, jak mam użyć tego drugiego zamiast pierwszego?

Z góry dziękuję!

Jared
źródło
1
Nie używałem .passThrough();tego w ten sposób, ale z dokumentacji, czy próbowałeś czegoś takiego: $httpBackend.expectGET('path/to/template.html'); // do action here $httpBackend.flush();Myślę, że to lepiej pasuje do twojego użycia - nie chcesz przechwytywać żądania, tj. whenGet(), Ale zamiast tego sprawdź, czy zostało wysłane, a następnie faktycznie Wyślij to?
Alex Osborn
1
Dziękuję za odpowiedź. Nie sądzę, żeby expectGETwysyłało to prośby ... przynajmniej po wyjęciu z pudełka. W dokumentach ich przykład /auth.pyma $httpBackend.whenprzed $httpBackend.expectGETi $httpBackend.flushwywołuje.
Jared
2
To prawda, expectGetwystarczy sprawdzić, czy próbowano wykonać żądanie.
Alex Osborn
1
Ach. Cóż, potrzebuję sposobu, aby powiedzieć $httpBackendmakiecie, aby faktycznie używał adresu URL podanego w dyrektywie templateUrli pobierał go. Myślałem, że passThroughto zrobię. Czy znasz inny sposób, aby to zrobić?
Jared
2
Hmm, nie wykonałem jeszcze wielu testów e2e, ale sprawdzając dokumenty - czy zamiast tego próbowałeś użyć zaplecza e2e - myślę, że dlatego nie masz metody passThrough - docs.angularjs.org/api/ngMockE2E.$httpBackend
Alex Osborn

Odpowiedzi:

187

Masz rację, że jest to związane z ngMock. Moduł ngMock jest automatycznie ładowany dla każdego testu Angular i inicjuje makietę w $httpBackendcelu obsługi dowolnego użycia $httpusługi, w tym pobierania szablonu. System szablonów próbuje załadować szablon $httpi staje się to „nieoczekiwanym żądaniem” do makiety.

Potrzebujesz sposobu, aby wstępnie załadować szablony do pliku, aby $templateCachebyły już dostępne, gdy Angular o nie poprosi, bez użycia $http.

Preferowane rozwiązanie: Karma

Jeśli używasz Karmy do uruchamiania testów (a powinieneś), możesz skonfigurować ją tak, aby ładowała szablony za pomocą preprocesora ng-html2js . Ng-html2js odczytuje określone pliki HTML i konwertuje je na moduł Angular, który wstępnie ładuje$templateCache .

Krok 1: Włącz i skonfiguruj preprocesor w swoim 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"
},

Jeśli używasz Yeoman do tworzenia szkieletów swojej aplikacji, ta konfiguracja będzie działać

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

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

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

Krok 2: Użyj modułu w swoich testach

// my-test.js

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

Aby uzyskać pełny przykład, spójrz na ten kanoniczny przykład autorstwa guru testów Angulara, Vojty Jiny . Obejmuje całą konfigurację: konfigurację karmy, szablony i testy.

Rozwiązanie niezwiązane z karmą

Jeśli z jakiegoś powodu nie używasz Karmy (miałem nieelastyczny proces kompilacji w starszej aplikacji) i testujesz tylko w przeglądarce, odkryłem, że możesz obejść przejęcie ngMocka, $httpBackendużywając surowego XHR, aby pobrać szablon naprawdę i włóż go do $templateCache. To rozwiązanie jest znacznie mniej elastyczne, ale na razie wykonuje swoje zadanie.

// 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);
}));

Ale naprawdę. Użyj Karmy . Konfiguracja wymaga trochę pracy, ale pozwala na uruchomienie wszystkich testów w wielu przeglądarkach jednocześnie z wiersza poleceń. Możesz więc mieć to jako część swojego systemu ciągłej integracji i / lub możesz uczynić z niego klawisz skrótu w swoim edytorze. Znacznie lepsze niż alt-tab-refresh-ad-infinitum.

SleepyMurph
źródło
6
Może to być oczywiste, ale jeśli inni utkną na tym samym i poszukają tutaj odpowiedzi: nie mogłem go uruchomić bez dodania preprocessorswzorca pliku (np. "path/to/templates/**/*.html") Do filessekcji w karma.conf.js.
Johan
1
Czy są więc jakieś poważne problemy z nie czekaniem na odpowiedź przed kontynuowaniem? Czy po prostu zaktualizuje wartość, gdy żądanie wróci (IE zajmuje 30 sekund)?
Jackie,
1
@Jackie Zakładam, że mówisz o przykładzie „nie karmy”, w którym używam falseparametru openwywołania XHR, aby uczynić go synchronicznym. Jeśli tego nie zrobisz, wykonanie będzie wesoło kontynuowane i rozpocznie wykonywanie testów, bez ładowania szablonu. To prowadzi z powrotem do tego samego problemu: 1) Żądanie szablonu zostaje wysłane. 2) Rozpocznie się wykonywanie testu. 3) Test kompiluje dyrektywę, a szablon nadal nie jest załadowany. 4) Angular żąda szablonu za pośrednictwem swojej $httpusługi, która jest wyszydzana. 5) Pozorowana $httpusługa narzeka: „nieoczekiwane żądanie”.
SleepyMurph
1
Byłem w stanie biegać na jaśminie bez Karmy.
FlavorScape
5
Kolejna rzecz: musisz zainstalować karma-ng-html2js-preprocessor ( npm install --save-dev karma-ng-html2js-preprocessor) i dodać go do sekcji wtyczek karma.conf.js, zgodnie ze stackoverflow.com/a/19077966/859631 .
Vincent,
37

Skończyło się na tym, że pobrałem pamięć podręczną szablonów i umieściłem tam widok. Nie mam kontroli nad tym, jak nie używam ngMock, okazuje się:

beforeEach(inject(function(_$rootScope_, _$compile_, $templateCache) {
    $scope = _$rootScope_;
    $compile = _$compile_;
    $templateCache.put('path/to/template.html', '<div>Here goes the template</div>');
}));
Jared
źródło
26
Oto moja skarga dotycząca tej metody ... Teraz, jeśli mamy zamiar mieć duży fragment html, który zamierzamy wstrzyknąć jako ciąg znaków do pamięci podręcznej szablonu, to co zrobimy, gdy zmienimy kod HTML z przodu ? Czy zmienić kod HTML w teście? IMO to niezrównoważona odpowiedź i powód, dla którego zdecydowaliśmy się na użycie opcji template zamiast templateUrl. Mimo że bardzo nie lubię umieszczać mojego html jako ogromnego ciągu w dyrektywie - jest to najbardziej zrównoważone rozwiązanie, aby nie musieć aktualizować dwóch miejsc HTML. Co nie wymaga dużo obrazowania, które HTML może z czasem nie pasować.
Sten Muchow,
12

Ten początkowy problem można rozwiązać, dodając:

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

Dzieje się tak , ponieważ domyślnie próbuje znaleźć $ httpBackend w module ngMock i nie jest pełny.

bzdury
źródło
1
Cóż, to rzeczywiście poprawna odpowiedź na pierwotne pytanie (to właśnie mi pomogło).
Mat
Próbowałem tego, ale passThrough () nadal nie działa dla mnie. Nadal generował błąd „Nieoczekiwane żądanie”.
frodo2975
8

Rozwiązanie, do którego doszedłem, wymaga jasmine-jquery.js i serwera proxy.

Wykonałem następujące kroki:

  1. W karma.conf:

dodaj jasmine-jquery.js do swoich plików

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

dodaj serwer proxy, który będzie obsługiwał twoje urządzenia

proxies = {
    '/' : 'http://localhost:3502/'
};
  1. W twojej specyfikacji

    opisać ('MySpec', function () {var $ scope, template; jasmine.getFixtures (). fixturesPath = 'public / parts /'; // niestandardowa ścieżka, dzięki czemu możesz podać rzeczywisty szablon, którego używasz w aplikacji beforeEach (function () {szablon = angular.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. Uruchom serwer w katalogu głównym aplikacji

    python -m SimpleHTTPServer 3502

  3. Uruchom karmę.

Zajęło mi trochę czasu, zanim to rozgryzłem, przeszukując wiele postów, myślę, że dokumentacja na ten temat powinna być bardziej przejrzysta, ponieważ jest to tak ważna kwestia.

Tomas Romero
źródło
Miałem problem z obsługą zasobów z localhost/base/specsi dodaniem serwera proxy po python -m SimpleHTTPServer 3502uruchomieniu go. Pan jest geniuszem!
pbojinov
Otrzymałem pusty element zwrócony z $ compile w moich testach. Inne miejsca sugerowały uruchomienie $ scope. $ Digest (): nadal pusty. Uruchomienie $ scope. $ Apply () zadziałało. Myślę, że to dlatego, że używam kontrolera w mojej dyrektywie? Niepewny. Dzięki za radę! Pomógł!
Sam Simmons
7

Moje rozwiązanie:

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'
],

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
źródło
Pierwsze przyzwoite rozwiązanie, które nie próbuje zmusić deweloperów do korzystania z Karmy. Dlaczego kanciastowie mieliby zrobić coś tak złego i łatwego do uniknięcia w środku czegoś tak fajnego? pfff
Fabio Milheiro
Widzę, że dodajesz „test / mock / ** / *. Js” i przypuszczam, że ma to załadować wszystkie fałszywe rzeczy, takie jak usługi i wszystko? Szukam sposobów uniknięcia powielania kodu fałszywych usług. Czy mógłbyś nam pokazać trochę więcej na ten temat?
Stephane
nie pamiętam dokładnie, ale prawdopodobnie były ustawienia na przykład JSON dla usługi $ http. Nic fajnego.
bartek
Miałem ten problem dzisiaj - świetne rozwiązanie. Używamy karmy, ale używamy też Chutzpah - nie ma powodu, abyśmy byli zmuszani do używania karmy, a tylko karma, aby móc testować dyrektywy.
lwalden
Używamy Django z Angularem, a to zadziałało jak urok przy testowaniu dyrektywy, która ładuje jej szablon templateUrl static, np. beforeEach(preloadTemplate(static_url +'seed/partials/beChartDropdown.html')); Dzięki!
Aleck Landgraf
6

Jeśli używasz Grunt, możesz użyć szablonów grunt-kątowych. Ładuje szablony w templateCache i jest przejrzysty dla konfiguracji specyfikacji.

Moja przykładowa konfiguracja:

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
źródło
6

Ten sam problem rozwiązałem w nieco inny sposób niż wybrane rozwiązanie.

  1. Najpierw zainstalowałem i skonfigurowałem wtyczkę ng-html2js dla karmy. W pliku 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. Następnie załadowałem moduł utworzony w beforeEach. W pliku Spec.js:

    beforeEach(module('myApp', 'to/templates/myTemplate.html'));
  3. Następnie użyłem $ templateCache.get, aby zapisać go w zmiennej. W pliku Spec.js:

    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. Wreszcie przetestowałem to w ten sposób. W pliku Spec.js:

    describe('element', function() {
      it('should contain the template', function() {
        expect(element.html()).toMatch(template);
      });
    });
glepretre
źródło
4

Aby dynamicznie załadować szablon html do $ templateCache, możesz po prostu użyć preprocesora karmy html2js, jak wyjaśniono tutaj

sprowadza się to do dodania szablonów „ .html” do plików w pliku conf.js, a także preprocessors = {' .html': 'html2js'};

I użyć

beforeEach(module('..'));

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

do pliku testowego js

Lior
źródło
DostajęUncaught SyntaxError: Unexpected token <
Melbourne2991,
2

jeśli używasz Karmy, rozważ użycie preprocesora karma-ng-html2js do wstępnej kompilacji zewnętrznych szablonów HTML i unikaj próbowania przez Angulara pobierania ich przez HTTP podczas wykonywania testu. Miałem z tym problem przez kilka naszych - w moim przypadku częściowe ścieżki templateUrl rozwiązały się podczas normalnego wykonywania aplikacji, ale nie podczas testów - z powodu różnic w strukturach aplikacji i katalogu testowego.

Nikita
źródło
2

Jeśli używasz wtyczki jasmine-maven-plugin razem z RequireJS, możesz użyć wtyczki tekstowej do załadowania zawartości szablonu do zmiennej, a następnie umieszczenia jej w pamięci podręcznej szablonów.


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
źródło
Czy możesz to zrobić bez Karmy?
Winnemucca
2

Jeśli używasz requirejs w swoich testach, możesz użyć wtyczki 'text', aby pobrać szablon html i umieścić go w $ templateCache.

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
źródło
0

Rozwiązuję ten problem, kompilując wszystkie szablony do templatecache. Używam łyka, podobne rozwiązanie można znaleźć też na chrząknięcie. Moje templateUrls w dyrektywach, wygląda jak modały

`templateUrl: '/templates/directives/sidebar/tree.html'`
  1. Dodaj nowy pakiet npm do mojego pliku package.json

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

  2. W pliku gulp dodaj szablon templatecache i nowe zadanie:

    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. Dodaj wszystkie pliki js w index.html

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

  4. Cieszyć się!

kitolog
źródło