Directiva de pruebas unitarias AngularJS con templateUrl

122

Tengo una directiva AngularJS que tiene una templateUrldefinida. Estoy tratando de probarlo con Jasmine.

Mi JavaScript de Jasmine se parece a lo siguiente, según la recomendación de esto :

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

        });
    });
});

Cuando ejecuto esto en mi error de especificación de Jasmine obtengo el siguiente error:

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

Todo lo que quiero es que la templateUrl se cargue tal cual, no quiero usar respond. Creo que esto puede estar relacionado con él usando ngMock en lugar de ngMockE2E . Si este es el culpable, ¿cómo uso el último en lugar del primero?

¡Gracias por adelantado!

Jared
fuente
1
No he utilizado .passThrough();de esa manera, pero a partir de la documentación, he intentado algo así como: $httpBackend.expectGET('path/to/template.html'); // do action here $httpBackend.flush();Creo que este se adapte a su uso mejor - no está queriendo atrapar la solicitud, es decir whenGet(), sino comprobar que se envía, y luego en realidad ¿mándalo?
Alex Osborn
1
Gracias por la respuesta. No creo que eso expectGETenvíe solicitudes ... al menos fuera de la caja. En los documentos de su ejemplo con /auth.pytiene una $httpBackend.whenantes de la $httpBackend.expectGETy $httpBackend.flushllamadas.
Jared
2
Eso es correcto, expectGetsolo está verificando si se intentó una solicitud.
Alex Osborn
1
Ah Bueno, necesito una manera de decirle al $httpBackendsimulacro que realmente use la URL provista en la directiva debajo templateUrle ir a buscarla. Pensé passThroughque haría esto. ¿Conoces una forma diferente de hacer esto?
Jared
2
Hmm, todavía no he hecho muchas pruebas de e2e, pero comprobando los documentos, ¿has intentado usar el backend de e2e en su lugar? Creo que es por eso que no tienes método passThrough - docs.angularjs.org/api/ngMockE2E.$httpBackend
Alex Osborn

Respuestas:

187

Tienes razón en que está relacionado con ngMock. El módulo ngMock se carga automáticamente para cada prueba angular, e inicializa el simulacro $httpBackendpara manejar cualquier uso del $httpservicio, que incluye la búsqueda de plantillas. El sistema de plantillas intenta cargar la plantilla $httpy se convierte en una "solicitud inesperada" para el simulacro.

Lo que necesita es una forma de cargar previamente las plantillas en el $templateCachepara que ya estén disponibles cuando Angular las solicite, sin usar $http.

La solución preferida: Karma

Si está utilizando Karma para ejecutar sus pruebas (y debería hacerlo), puede configurarlo para cargar las plantillas con el preprocesador ng-html2js . Ng-html2js lee los archivos HTML que especifique y los convierte en un módulo angular que precarga el $templateCache.

Paso 1: habilite y configure el preprocesador en su 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 está utilizando Yeoman para andamiar su aplicación, esta configuración funcionará

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

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

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

Paso 2: usa el módulo en tus pruebas

// my-test.js

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

Para un ejemplo completo, mira este ejemplo canónico del gurú de la prueba angular Vojta Jina . Incluye una configuración completa: configuración de karma, plantillas y pruebas.

Una solución sin karma

Si no usa Karma por alguna razón (tuve un proceso de compilación inflexible en la aplicación heredada) y solo estoy probando en un navegador, he descubierto que puede evitar la adquisición de ngMock $httpBackendutilizando un XHR sin procesar para obtener la plantilla de verdad e insértelo en el $templateCache. Esta solución es mucho menos flexible, pero hace el trabajo por ahora.

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

Hablando en serio. Usa Karma . La configuración requiere un poco de trabajo, pero le permite ejecutar todas sus pruebas, en varios navegadores a la vez, desde la línea de comandos. Por lo tanto, puede tenerlo como parte de su sistema de integración continua y / o puede convertirlo en una tecla de acceso directo de su editor. Mucho mejor que alt-tab-refresh-ad-infinitum.

SleepyMurph
fuente
66
Esto puede ser obvio, pero si otros se atascan en lo mismo y buscan respuestas: no podría hacerlo funcionar sin agregar también el preprocessorspatrón de archivo (por ejemplo "path/to/templates/**/*.html") a la filessección en karma.conf.js.
Johan
1
Entonces, ¿hay algún problema importante con no esperar la respuesta antes de continuar? ¿Solo actualizará el valor cuando vuelva la solicitud (IE tarda 30 segundos)?
Jackie
1
@Jackie Supongo que estás hablando del ejemplo "no Karma" en el que uso el falseparámetro para la openllamada de XHR para hacerlo sincrónico. Si no hace eso, la ejecución continuará alegremente y comenzará a ejecutar sus pruebas, sin tener la plantilla cargada. Eso lo lleva de vuelta al mismo problema: 1) Se apaga la solicitud de plantilla. 2) La prueba comienza a ejecutarse. 3) La prueba compila una directiva y la plantilla aún no está cargada. 4) Angular solicita la plantilla a través de su $httpservicio, que se burla. 5) El $httpservicio simulado se queja: "solicitud inesperada".
SleepyMurph
1
Pude correr gruñido de jazmín sin Karma.
FlavorScape
55
Otra cosa: debe instalar karma-ng-html2js-preprocessor ( npm install --save-dev karma-ng-html2js-preprocessor) y agregarlo a la sección de complementos de su karma.conf.js, de acuerdo con stackoverflow.com/a/19077966/859631 .
Vincent
37

Lo que terminé haciendo fue obtener el caché de la plantilla y poner la vista allí. No tengo control sobre no usar ngMock, resulta que:

beforeEach(inject(function(_$rootScope_, _$compile_, $templateCache) {
    $scope = _$rootScope_;
    $compile = _$compile_;
    $templateCache.put('path/to/template.html', '<div>Here goes the template</div>');
}));
Jared
fuente
26
Aquí está mi queja con este método ... Ahora, si vamos a tener una gran pieza de html que vamos a inyectar como una cadena en el caché de la plantilla, ¿qué haremos cuando cambiemos el html en la parte frontal? ? ¿Cambiar el html en la prueba también? En mi opinión, esa es una respuesta insostenible y la razón por la que utilizamos la opción template over templateUrl. Aunque no me gusta mucho tener mi html como una cadena masiva en la directiva, es la solución más sostenible para no tener que actualizar dos lugares de html. Lo que no toma muchas imágenes que el html con el tiempo no coincida.
Sten Muchow
12

Este problema inicial se puede resolver agregando esto:

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

Eso es porque intenta encontrar $ httpBackend en el módulo ngMock de forma predeterminada y no está lleno.

bullgare
fuente
1
Bueno, esa es la respuesta correcta a la pregunta original (esa es la que me ayudó).
Mat
Intenté esto, pero passThrough () todavía no funcionó para mí. Todavía dio el error "Solicitud inesperada".
frodo2975
8

La solución a la que llegué necesita jasmine-jquery.js y un servidor proxy.

Seguí estos pasos:

  1. En karma.conf:

agregue jasmine-jquery.js a sus archivos

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

agregue un servidor proxy que sirva sus instalaciones

proxies = {
    '/' : 'http://localhost:3502/'
};
  1. En su especificación

    describe ('MySpec', function () {var $ scope, template; jasmine.getFixtures (). fixturesPath = 'public / partials /'; // ruta personalizada para que pueda servir la plantilla real que usa en la aplicación antes de cada función () {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. Ejecute un servidor en el directorio raíz de su aplicación

    python -m SimpleHTTPServer 3502

  3. Ejecuta karma.

Me tomó un tiempo resolver esto, teniendo que buscar muchas publicaciones, creo que la documentación sobre esto debería ser más clara, ya que es un tema tan importante.

Tomás Romero
fuente
Estaba teniendo problemas para servir activos localhost/base/specsy agregar un servidor proxy con la python -m SimpleHTTPServer 3502ejecución reparada. ¡Usted señor es un genio!
pbojinov
Estaba obteniendo un elemento vacío devuelto por $ compile en mis pruebas. Otros lugares sugirieron ejecutar $ scope. $ Digest (): todavía vacío. Sin embargo, ejecutar $ scope. $ Apply () funcionó. ¿Creo que fue porque estoy usando un controlador en mi directiva? No estoy seguro. ¡Gracias por el consejo! ¡Ayudado!
Sam Simmons
7

Mi solución:

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

la prueba:

'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
fuente
Primera solución decente que no intenta obligar a los desarrolladores a usar Karma. ¿Por qué los chicos angulosos harían algo tan malo y fácilmente evitable en medio de algo tan genial? pfff
Fabio Milheiro
Veo que agregas un 'test / simulacro / ** / *. Js' y supongo que es para cargar todas las cosas simuladas como servicios y todo. Estoy buscando formas de evitar la duplicación de código de los servicios simulados. ¿Nos mostrarías un poco más sobre eso?
Stephane
no recuerdo exactamente, pero había configuraciones probables, por ejemplo, JSON para el servicio $ http. Nada sofisticado.
bartek
Tuve este problema hoy - gran solución. Usamos karma pero también usamos Chutzpah; no hay razón para que nos veamos obligados a usar karma y solo karma para poder someter a prueba las directivas.
lwalden
Estamos usando Django con Angular, y esto funcionó de maravilla para probar una directiva que carga su templateUrl static, por ejemplo, beforeEach(preloadTemplate(static_url +'seed/partials/beChartDropdown.html')); ¡Gracias!
Aleck Landgraf
6

Si está usando Grunt, puede usar grunt-angular-templates. Carga sus plantillas en templateCache y es transparente a la configuración de sus especificaciones.

Mi configuración de muestra:

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

};
Tomás Romero
fuente
6

Resolví el mismo problema de una manera ligeramente diferente a la solución elegida.

  1. Primero, instalé y configuré el complemento ng-html2js para karma. En el archivo 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. Luego cargué el módulo creado en beforeEach. En su archivo Spec.js:

    beforeEach(module('myApp', 'to/templates/myTemplate.html'));
  3. Luego usé $ templateCache.get para almacenarlo en una variable. En su archivo 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. Finalmente, lo probé de esta manera. En su archivo Spec.js:

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

Para cargar la plantilla html dinámicamente en $ templateCache, puede usar el preprocesador de karma html2js, como se explica aquí

esto se reduce a agregar plantillas ' .html' a sus archivos en el archivo conf.js, así como preprocesadores = {' .html': 'html2js'};

y use

beforeEach(module('..'));

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

en su archivo de prueba js

Lior
fuente
Estoy recibiendoUncaught SyntaxError: Unexpected token <
Melbourne2991
2

si está usando Karma, considere usar karma-ng-html2js-preprocessor para precompilar sus plantillas HTML externas y evitar que Angular intente HTTP GET durante la ejecución de la prueba. Luché con esto por un par de los nuestros, en mi caso, las rutas parciales de templateUrl se resolvieron durante la ejecución normal de la aplicación, pero no durante las pruebas, debido a las diferencias en las estructuras de directorios de la aplicación frente a la prueba.

Nikita
fuente
2

Si está utilizando el complemento jazmín-maven junto con RequireJS, puede usar el complemento de texto para cargar el contenido de la plantilla en una variable y luego colocarlo en el caché de la plantilla.


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
fuente
¿Puedes hacer esto sin Karma?
Winnemucca
2

Si usa requirejs en sus pruebas, puede usar el plugin 'text' para extraer la plantilla html y ponerla en $ 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
fuente
0

Resuelvo este problema compilando todas las plantillas en templatecache. Estoy usando gulp, también puedes encontrar una solución similar para gruñir. Mi plantilla se rige en directivas, parece modales

`templateUrl: '/templates/directives/sidebar/tree.html'`
  1. Agregar un nuevo paquete npm en mi package.json

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

  2. En el archivo gulp, agregue templatecache y una nueva tarea:

    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. Agregue todos los archivos js en index.html

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

  4. ¡Disfrutar!

kitolog
fuente