Direttiva di Unit Testing AngularJS con templateUrl


122

Ho una direttiva AngularJS che ha un file templateUrl. Sto provando a testarlo con Jasmine.

Il mio JavaScript Jasmine è simile al seguente, secondo la raccomandazione di questo :

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

        });
    });
});

Quando lo eseguo nel mio errore di specifica Jasmine ottengo il seguente errore:

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

Tutto quello che voglio è che templateUrl venga caricato così com'è, non voglio usare respond. Credo che questo possa essere correlato ad esso utilizzando ngMock invece di ngMockE2E . Se questo è il colpevole, come posso utilizzare quest'ultimo al posto del primo?

Grazie in anticipo!


1
Non l'ho usato .passThrough();in questo modo, ma dai documenti, hai provato qualcosa del tipo: $httpBackend.expectGET('path/to/template.html'); // do action here $httpBackend.flush();Penso che questo si adatti meglio al tuo utilizzo - non vuoi catturare la richiesta, cioè whenGet(), ma invece controllare che sia inviata, e poi effettivamente invialo?
Alex Osborn

1
Grazie per la risposta. Non credo che expectGETinvii richieste ... almeno fuori dagli schemi. Nei documenti il loro esempio con /auth.pyha una $httpBackend.whenprima di $httpBackend.expectGETe $httpBackend.flushchiama.
Jared

2
È corretto, expectGetsta solo controllando se è stata tentata una richiesta.
Alex Osborn

1
Ah. Bene, ho bisogno di un modo per dire al $httpBackendmock di utilizzare effettivamente l'URL fornito nella direttiva sotto templateUrle andare a prenderlo. Ho pensato di passThroughfarlo. Conosci un modo diverso per farlo?
Jared

2
Hmm, non ho ancora fatto molti test e2e, ma controllando i documenti - hai provato a usare il backend e2e invece - penso che sia per questo che non hai il metodo passThrough - docs.angularjs.org/api/ngMockE2E.$httpBackend
Alex Osborn

Risposte:


187

Hai ragione sul fatto che è correlato a ngMock. Il modulo ngMock viene caricato automaticamente per ogni test Angular e inizializza il mock$httpBackend per gestire qualsiasi utilizzo del $httpservizio, incluso il recupero dei modelli. Il sistema di template cerca di caricare il template $httpe diventa una "richiesta inaspettata" per il mock.

Quello che ti serve è un modo per precaricare i modelli nel file $templateCache modo che siano già disponibili quando Angular li richiede, senza utilizzarli$http .

La soluzione preferita: il karma

Se stai usando Karma per eseguire i tuoi test (e dovresti esserlo), puoi configurarlo per caricare i modelli per te con ng-html2js preprocessore . Ng-html2js legge i file HTML specificati e li converte in un modulo Angular che precarica il$templateCache .

Passaggio 1: abilita e configura il preprocessore nel tuo file 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"
},

Se stai usando Yeoman per impalcare la tua app, questa configurazione funzionerà

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

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

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

Passaggio 2: utilizzare il modulo nei test

// my-test.js

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

Per un esempio completo, guarda questo esempio canonico del guru dei test angolari Vojta Jina . Include un'intera configurazione: configurazione karma, modelli e test.

Una soluzione non-karma

Se non usi Karma per qualsiasi motivo (avevo un processo di compilazione inflessibile nell'app legacy) e stai solo testando in un browser, ho scoperto che puoi aggirare l'acquisizione di $httpBackendngMock usando un XHR grezzo per recuperare il modello per davvero e inseriscilo nel file $templateCache. Questa soluzione è molto meno flessibile, ma per ora porta a termine il lavoro.

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

Seriamente però. Usa Karma . Ci vuole un po 'di lavoro per la configurazione, ma ti consente di eseguire tutti i tuoi test, in più browser contemporaneamente, dalla riga di comando. Quindi puoi averlo come parte del tuo sistema di integrazione continua e / o puoi renderlo un tasto di scelta rapida dal tuo editor. Molto meglio di alt-tab-refresh-ad-infinitum.


6
Questo può essere ovvio, ma se altri si bloccano sulla stessa cosa e cercano qui le risposte: non potrei farlo funzionare senza aggiungere anche il preprocessorsmodello di file (ad esempio "path/to/templates/**/*.html") alla filessezione in karma.conf.js.
Johan

1
Quindi ci sono grossi problemi nel non aspettare la risposta prima di continuare? Aggiornerà semplicemente il valore quando la richiesta torna (IE richiede 30 secondi)?
Jackie

1
@ Jackie Presumo che tu stia parlando dell'esempio "non-Karma" in cui utilizzo il falseparametro per la openchiamata di XHR per renderlo sincrono. Se non lo fai, l'esecuzione continuerà allegramente e inizierà a eseguire i tuoi test, senza caricare il modello. Questo ti riporta indietro allo stesso problema: 1) La richiesta del modello si spegne. 2) Il test inizia l'esecuzione. 3) Il test compila una direttiva e il modello non è ancora caricato. 4) Angular richiede il template tramite il proprio $httpservizio, che viene deriso. 5) Il finto $httpservizio si lamenta: "richiesta inaspettata".
SleepyMurph

1
Sono stato in grado di eseguire grugnito-gelsomino senza Karma.
FlavorScape

5
Un'altra cosa: devi installare karma-ng-html2js-preprocessor ( npm install --save-dev karma-ng-html2js-preprocessor) e aggiungerlo alla sezione plugin del tuo karma.conf.js, secondo stackoverflow.com/a/19077966/859631 .
Vincent

37

Quello che ho finito per fare è stato ottenere la cache dei modelli e inserire la vista lì. Non ho il controllo sul non utilizzare ngMock, risulta:

beforeEach(inject(function(_$rootScope_, _$compile_, $templateCache) {
    $scope = _$rootScope_;
    $compile = _$compile_;
    $templateCache.put('path/to/template.html', '<div>Here goes the template</div>');
}));

26
Ecco la mia lamentela con questo metodo ... Ora se avremo un grosso pezzo di html che inietteremo come stringa nella cache del modello, allora cosa faremo quando cambieremo l'html sul front-end ? Cambiare anche l'html nel test? IMO che è una risposta insostenibile e il motivo per cui abbiamo scelto di utilizzare l'opzione template su templateUrl. Anche se non mi piace molto avere il mio html come una stringa enorme nella direttiva, è la soluzione più sostenibile per non dover aggiornare due posizioni di html. Il che non richiede molte immagini che l'html può non corrispondere nel tempo.
Sten Muchow

12

Questo problema iniziale può essere risolto aggiungendo questo:

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

Questo perché cerca di trovare $ httpBackend nel modulo ngMock per impostazione predefinita e non è pieno.


1
Beh, questa è davvero la risposta corretta alla domanda originale (è quella che mi ha aiutato).
Mat

Ho provato, ma passThrough () continuava a non funzionare per me. Ha ancora fornito l'errore "Richiesta imprevista".
frodo2975

8

La soluzione che ho raggiunto richiede jasmine-jquery.js e un server proxy.

Ho seguito questi passaggi:

  1. In karma.conf:

aggiungi jasmine-jquery.js ai tuoi file

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

aggiungi un server proxy che servirà i tuoi dispositivi

proxies = {
    '/' : 'http://localhost:3502/'
};
  1. Nella tua spec

    Descrivi ('MySpec', function () {var $ scope, template; jasmine.getFixtures (). fixturesPath = 'public / partials /'; // percorso personalizzato in modo da poter servire il modello reale che usi sull'app prima di ogni (funzione () {template = 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. Esegui un server nella directory principale della tua app

    python -m SimpleHTTPServer 3502

  3. Esegui karma.

Mi ci è voluto un po 'per capirlo, dovendo cercare molti post, penso che la documentazione su questo dovrebbe essere più chiara, poiché è una questione così importante.


Ho avuto problemi a fornire risorse localhost/base/specse aggiungere un server proxy con l' python -m SimpleHTTPServer 3502esecuzione risolto. Lei, signore, è un genio!
pbojinov

Stavo ottenendo un elemento vuoto restituito da $ compile nei miei test. Altri posti hanno suggerito di eseguire $ scope. $ Digest (): ancora vuoto. Tuttavia, l'esecuzione di $ scope. $ Apply () ha funzionato. Penso che sia stato perché sto usando un controller nella mia direttiva? Non sono sicuro. Grazie per il consiglio! Aiutato!
Sam Simmons

7

La mia soluzione:

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

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

Prima soluzione decente che non cerca di forzare gli sviluppatori a usare Karma. Perché i ragazzi spigolosi dovrebbero fare qualcosa di così brutto e facilmente evitabile nel mezzo di qualcosa di così bello? pfff
Fabio Milheiro

Vedo che aggiungi un "test / mock / ** / *. Js" e suppongo che sia per caricare tutte le cose derise come i servizi e tutto il resto? Sto cercando modi per evitare la duplicazione del codice di servizi fittizi. Ci mostreresti qualcosa di più su questo?
Stephane

non ricordo esattamente, ma probabilmente c'erano impostazioni per esempio JSON per il servizio $ http. Nulla di bello.
bartek

Ho avuto questo problema oggi - ottima soluzione. Usiamo karma ma usiamo anche Chutzpah - non c'è motivo per cui dovremmo essere costretti a usare karma e solo karma per essere in grado di testare le direttive.
lwalden

Stiamo usando Django con Angular, e questo ha funzionato come un fascino per testare una direttiva che carica il suo templateUrl static, ad esempio, beforeEach(preloadTemplate(static_url +'seed/partials/beChartDropdown.html')); grazie!
Aleck Landgraf

6

Se stai usando Grunt, puoi usare grunt-angular-templates. Carica i tuoi modelli nella templateCache ed è trasparente alla configurazione delle tue specifiche.

La mia configurazione di esempio:

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

};

6

Ho risolto lo stesso problema in un modo leggermente diverso dalla soluzione scelta.

  1. Per prima cosa, ho installato e configurato il plugin ng-html2js per karma. Nel file 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. Quindi ho caricato il modulo creato nel file beforeEach. Nel tuo file Spec.js:

    beforeEach(module('myApp', 'to/templates/myTemplate.html'));
  3. Quindi ho usato $ templateCache.get per memorizzarlo in una variabile. Nel tuo file 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. Alla fine l'ho testato in questo modo. Nel tuo file Spec.js:

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

4

Per caricare il template html dinamicamente in $ templateCache potresti semplicemente usare il pre-processore karma html2js, come spiegato qui

questo si riduce all'aggiunta di modelli " .html" ai file nel file conf.js e preprocessori = {" .html': 'html2js'};

e utilizzare

beforeEach(module('..'));

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

nel tuo file di test js


Sto ottenendoUncaught SyntaxError: Unexpected token <
Melbourne2991

2

se stai usando Karma, considera l'utilizzo di karma-ng-html2js-preprocessor per precompilare i tuoi modelli HTML esterni ed evitare che Angular provi a HTTP GET durante l'esecuzione del test. Ho lottato con questo per un paio di nostri - nel mio caso i percorsi parziali di templateUrl sono stati risolti durante la normale esecuzione dell'app ma non durante i test - a causa delle differenze nelle strutture di app rispetto a test dir.


2

Se stai usando il plugin jasmine- maven insieme a RequireJS puoi usare il plugin di testo per caricare il contenuto del modello in una variabile e poi metterlo nella cache del modello.


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

    });
});

Puoi farlo senza Karma?
Winnemucca

2

Se usi requirejs nei tuoi test, puoi usare il plugin 'text' per inserire il template html e metterlo in $ 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();
    }));
  });
});

0

Risolvo questo problema compilando tutti i modelli in templatecache. Sto usando gulp, puoi trovare una soluzione simile anche per grugnito. My templateUrls in directives, modals assomiglia

`templateUrl: '/templates/directives/sidebar/tree.html'`
  1. Aggiungi un nuovo pacchetto npm nel mio package.json

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

  2. Nel file gulp aggiungi templatecache e una nuova attività:

    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. Aggiungi tutti i file js in index.html

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

  4. Godere!

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.