Directive AngularJS de test unitaire avec templateUrl

122

J'ai une directive AngularJS qui a un templateUrlfichier. J'essaye de le tester unitaire avec Jasmine.

Mon JavaScript Jasmine ressemble à ce qui suit, selon la recommandation de ceci :

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

        });
    });
});

Lorsque je l'exécute dans mon erreur de spécification Jasmine, j'obtiens l'erreur suivante:

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

Tout ce que je veux, c'est que le templateUrl soit chargé tel quel - je ne veux pas l'utiliser respond. Je pense que cela peut être lié à l'utilisation de ngMock au lieu de ngMockE2E . Si c'est le coupable, comment utiliser ce dernier au lieu du premier?

Merci d'avance!

Jared
la source
1
Je ne l'ai pas utilisé .passThrough();de cette façon, mais à partir de la documentation, avez-vous essayé quelque chose comme: $httpBackend.expectGET('path/to/template.html'); // do action here $httpBackend.flush();Je pense que cela correspond mieux à votre utilisation - vous ne voulez pas attraper la demande, c'est whenGet()-à- dire , mais vérifiez plutôt qu'elle est envoyée, puis effectivement envoie-le?
Alex Osborn
1
Merci pour la réponse. Je ne pense pas que cela expectGETenvoie des demandes ... du moins hors de la boîte. Dans la documentation, leur exemple avec /auth.pya un $httpBackend.whenavant les appels $httpBackend.expectGETet $httpBackend.flush.
Jared
2
C'est exact, il expectGets'agit simplement de vérifier si une demande a été tentée.
Alex Osborn
1
Ah. Eh bien, j'ai besoin d'un moyen de dire à la $httpBackendmaquette d'utiliser réellement l'URL fournie dans la directive ci-dessous templateUrlet d'aller la chercher. Je pensais que passThroughje ferais ça. Connaissez-vous une autre façon de procéder?
Jared
2
Hmm, je n'ai pas encore fait beaucoup de tests e2e, mais en vérifiant la documentation - avez-vous essayé d'utiliser le backend e2e à la place - je pense que c'est pourquoi vous n'avez pas de méthode passThrough - docs.angularjs.org/api/ngMockE2E.$httpBackend
Alex Osborn

Réponses:

187

Vous avez raison de dire que c'est lié à ngMock. Le module ngMock est automatiquement chargé pour chaque test angulaire et initialise la maquette $httpBackendpour gérer toute utilisation du $httpservice, qui inclut la récupération de modèle. Le système de modèles essaie de charger le modèle $httpet cela devient une «demande inattendue» pour le simulacre.

Ce dont vous avez besoin, un moyen de pré-charger les modèles dans le $templateCacheafin qu'ils soient déjà disponibles lorsque Angular les demande, sans utiliser $http.

La solution préférée: le karma

Si vous utilisez Karma pour exécuter vos tests (et vous devriez l'être), vous pouvez le configurer pour charger les modèles pour vous avec le préprocesseur ng-html2js . Ng-html2js lit les fichiers HTML que vous spécifiez et les convertit en un module angulaire qui précharge le fichier $templateCache.

Étape 1: Activez et configurez le préprocesseur dans votre 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"
},

Si vous utilisez Yeoman pour échafauder votre application, cette configuration fonctionnera

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

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

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

Étape 2: Utilisez le module dans vos tests

// my-test.js

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

Pour un exemple complet, regardez cet exemple canonique du gourou du test angulaire Vojta Jina . Il comprend une configuration entière: configuration de karma, modèles et tests.

Une solution non-karma

Si vous n'utilisez pas Karma pour une raison quelconque (j'ai eu un processus de construction inflexible dans l'application héritée) et que vous testez simplement dans un navigateur, j'ai trouvé que vous pouvez contourner la prise de contrôle de ngMock $httpBackenden utilisant un XHR brut pour récupérer le modèle pour de vrai et insérez-le dans le fichier $templateCache. Cette solution est beaucoup moins flexible, mais elle fait le travail pour le moment.

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

Sérieusement. Utilisez Karma . La configuration demande un peu de travail, mais elle vous permet d'exécuter tous vos tests, dans plusieurs navigateurs à la fois, à partir de la ligne de commande. Vous pouvez donc l'inclure dans votre système d'intégration continue et / ou en faire une touche de raccourci depuis votre éditeur. Bien mieux que alt-tab-refresh-ad-infinitum.

SleepyMurph
la source
6
Cela peut être évident, mais si d'autres restent bloqués sur la même chose et cherchent ici des réponses: je ne pourrais pas le faire fonctionner sans ajouter également le preprocessorsmodèle de fichier (par exemple "path/to/templates/**/*.html") à la filessection dans karma.conf.js.
Johan
1
Y a-t-il donc des problèmes majeurs à ne pas attendre la réponse avant de continuer? Va-t-il simplement mettre à jour la valeur lorsque la demande revient (IE prend 30 secondes)?
Jackie
1
@Jackie Je suppose que vous parlez de l'exemple "non-Karma" où j'utilise le falseparamètre pour l'appel du XHR openpour le rendre synchrone. Si vous ne le faites pas, l'exécution se poursuivra joyeusement et commencera à exécuter vos tests, sans que le modèle ne soit chargé. Cela vous ramène directement au même problème: 1) La demande de modèle est envoyée. 2) Le test commence à s'exécuter. 3) Le test compile une directive et le modèle n'est toujours pas chargé. 4) Angular demande le modèle via son $httpservice, qui est simulé. 5) Le $httpservice simulé se plaint: "demande inattendue".
SleepyMurph
1
J'ai pu courir grunt-jasmine sans Karma.
FlavorScape
5
Autre chose: vous devez installer karma-ng-html2js-preprocessor ( npm install --save-dev karma-ng-html2js-preprocessor) et l'ajouter à la section plugins de votre karma.conf.js, selon stackoverflow.com/a/19077966/859631 .
Vincent
37

Ce que j'ai fini par faire, c'est obtenir le cache du modèle et y mettre la vue. Je n'ai pas le contrôle de ne pas utiliser ngMock, il s'avère que:

beforeEach(inject(function(_$rootScope_, _$compile_, $templateCache) {
    $scope = _$rootScope_;
    $compile = _$compile_;
    $templateCache.put('path/to/template.html', '<div>Here goes the template</div>');
}));
Jared
la source
26
Voici ma plainte concernant cette méthode ... Maintenant, si nous allons avoir un gros morceau de html que nous allons injecter sous forme de chaîne dans le cache du modèle, que ferons-nous lorsque nous changerons le html sur le front-end ? Changer également le code HTML dans le test? IMO qui est une réponse insoutenable et la raison pour laquelle nous avons opté pour l'option template over templateUrl. Même si je n'aime pas du tout avoir mon html en tant que chaîne massive dans la directive - c'est la solution la plus durable pour ne pas avoir à mettre à jour deux emplacements de html. Ce qui ne prend pas beaucoup d'imagerie que le html peut au fil du temps ne pas correspondre.
Sten Muchow
12

Ce problème initial peut être résolu en ajoutant ceci:

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

C'est parce qu'il essaie de trouver $ httpBackend dans le module ngMock par défaut et qu'il n'est pas plein.

bullgare
la source
1
Eh bien, c'est la bonne réponse à la question initiale (c'est celle qui m'a aidé).
Mat
J'ai essayé, mais passThrough () ne fonctionnait toujours pas pour moi. Il a toujours donné l'erreur "Demande inattendue".
frodo2975
8

La solution que j'ai atteinte nécessite jasmine-jquery.js et un serveur proxy.

J'ai suivi ces étapes:

  1. Dans karma.conf:

ajoutez jasmine-jquery.js à vos fichiers

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

ajoutez un serveur proxy qui servira vos appareils

proxies = {
    '/' : 'http://localhost:3502/'
};
  1. Dans vos spécifications

    describe ('MySpec', function () {var $ scope, template; jasmine.getFixtures (). fixturesPath = 'public / partials /'; // chemin personnalisé afin que vous puissiez servir le modèle réel que vous utilisez sur l'application beforeEach (function () {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. Exécutez un serveur sur le répertoire racine de votre application

    python -m SimpleHTTPServer 3502

  3. Exécutez le karma.

Il m'a fallu un certain temps pour comprendre cela, ayant à rechercher de nombreux messages, je pense que la documentation à ce sujet devrait être plus claire, car c'est une question très importante.

Tomas Romero
la source
J'avais du mal à servir les actifs localhost/base/specset à ajouter un serveur proxy en python -m SimpleHTTPServer 3502exécutant le corrigé. Vous monsieur êtes un génie!
pbojinov
Je recevais un élément vide renvoyé par $ compile dans mes tests. D'autres endroits suggéraient d'exécuter $ scope. $ Digest (): toujours vide. L'exécution de $ scope. $ Apply () a cependant fonctionné. Je pense que c'est parce que j'utilise un contrôleur dans ma directive? Pas certain. Merci pour le conseil! Aidé!
Sam Simmons
7

Ma solution:

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

le 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
la source
Première solution décente qui n'essaye pas de forcer les développeurs à utiliser Karma. Pourquoi les gars anguleux feraient-ils quelque chose de si mauvais et facilement évitable au milieu de quelque chose d'aussi cool? pfff
Fabio Milheiro
Je vois que vous ajoutez un 'test / mock / ** / *. Js' et je suppose que c'est pour charger tous les trucs moqués comme les services et tout? Je cherche des moyens d'éviter la duplication de code de services simulés. Pourriez-vous nous en montrer un peu plus à ce sujet?
Stephane
Je ne me souviens pas exactement, mais il y avait probablement des paramètres par exemple des JSON pour le service $ http. Rien d'extraordinaire.
bartek le
Eu ce problème aujourd'hui - excellente solution. Nous utilisons le karma mais nous utilisons également Chutzpah - aucune raison pour laquelle nous devrions être obligés d'utiliser le karma et seulement le karma pour pouvoir tester des directives unitaires.
lwalden
Nous utilisons Django avec Angular, et cela a fonctionné comme un charme pour tester une directive qui charge son templateUrl static, par exemple beforeEach(preloadTemplate(static_url +'seed/partials/beChartDropdown.html')); Merci!
Aleck Landgraf
6

Si vous utilisez Grunt, vous pouvez utiliser grunt-angular-templates. Il charge vos modèles dans le templateCache et il est transparent à la configuration de vos spécifications.

Mon exemple de configuration:

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
la source
6

J'ai résolu le même problème d'une manière légèrement différente de la solution choisie.

  1. Tout d'abord, j'ai installé et configuré le plugin ng-html2js pour le karma. Dans le fichier 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. Ensuite, j'ai chargé le module créé dans le fichier beforeEach. Dans votre fichier Spec.js:

    beforeEach(module('myApp', 'to/templates/myTemplate.html'));
  3. Ensuite, j'ai utilisé $ templateCache.get pour le stocker dans une variable. Dans votre fichier 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. Enfin, je l'ai testé de cette façon. Dans votre fichier Spec.js:

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

Pour charger dynamiquement le template HTML dans $ templateCache, vous pouvez simplement utiliser le pré-processeur karma html2js, comme expliqué ici

cela revient à ajouter des modèles ' .html' à vos fichiers dans le fichier conf.js ainsi que preprocessors = {' .html': 'html2js'};

et utilise

beforeEach(module('..'));

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

dans votre fichier de test js

Lior
la source
Je reçoisUncaught SyntaxError: Unexpected token <
Melbourne2991
2

si vous utilisez Karma, pensez à utiliser karma-ng-html2js-preprocessor pour pré-compiler vos modèles HTML externes et éviter que Angular essaie de les obtenir par HTTP pendant l'exécution du test. J'ai eu du mal avec cela pour quelques-uns des nôtres - dans mon cas, les chemins partiels de templateUrl ont été résolus pendant l'exécution normale de l'application mais pas pendant les tests - en raison de différences dans les structures des répertoires d'application et de test.

Nikita
la source
2

Si vous utilisez le jasmine-maven-plugin avec RequireJS, vous pouvez utiliser le plugin de texte pour charger le contenu du modèle dans une variable, puis le mettre dans le cache du modèle.


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
la source
Pouvez-vous faire cela sans Karma?
Winnemucca
2

Si vous utilisez requirejs dans vos tests, vous pouvez utiliser le plugin 'text' pour extraire le modèle html et le mettre dans $ 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
la source
0

Je résous ce problème en compilant tous les modèles en templatecache. J'utilise gulp, vous pouvez également trouver une solution similaire pour grunt. Mon templateUrls dans les directives, les modaux ressemblent

`templateUrl: '/templates/directives/sidebar/tree.html'`
  1. Ajouter un nouveau package npm dans mon package.json

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

  2. Dans le fichier gulp, ajoutez templatecache et une nouvelle tâche:

    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. Ajouter tous les fichiers js dans index.html

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

  4. Prendre plaisir!

kitolog
la source