Diretiva de teste de unidade AngularJS com templateUrl

122

Eu tenho uma diretiva AngularJS que tem um templateUrldefinido. Estou tentando testá-lo com Jasmine.

Meu Jasmine JavaScript parece com o seguinte, por recomendação do presente :

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 executo isso no meu erro de especificação do Jasmine, recebo o seguinte erro:

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

Tudo o que eu quero é que o templateUrl seja carregado como está - eu não quero usar respond. Eu acredito que isso pode estar relacionado a ele usando ngMock em vez de ngMockE2E . Se esse é o culpado, como eu uso o último em vez do primeiro?

Desde já, obrigado!

Jared
fonte
1
Eu não usei .passThrough();dessa maneira, mas com base nos documentos, você tentou algo como: $httpBackend.expectGET('path/to/template.html'); // do action here $httpBackend.flush();Eu acho que isso se encaixa melhor no seu uso - você não deseja capturar a solicitação, ou seja whenGet(), mas verifique se ela é enviada e, na verdade, Envie isto?
Alex Osborn
1
Obrigado pela resposta. Acho que não expectGETenvia pedidos ... pelo menos fora da caixa. Nos documentos, o exemplo deles /auth.pytem um $httpBackend.whenantes das chamadas $httpBackend.expectGETe $httpBackend.flush.
Jared
2
Correto, expectGeté apenas verificar se uma solicitação foi tentada.
Alex Osborn
1
Ah Bem, eu preciso de uma maneira de dizer ao $httpBackendmock para realmente usar a URL fornecida na diretiva templateUrle ir buscá-la. Eu pensei passThroughque faria isso. Você conhece uma maneira diferente de fazer isso?
Jared
2
Hmm, ainda não fiz muitos testes do e2e, mas verificando os documentos - você tentou usar o back-end do e2e - acho que é por isso que você não obteve nenhum método de passagem - docs.angularjs.org/api/ngMockE2E.$httpBackend
Alex Osborn

Respostas:

187

Você está certo de que está relacionado ao ngMock. O módulo ngMock é carregado automaticamente para cada teste Angular e inicializa a simulação $httpBackendpara lidar com qualquer uso do $httpserviço, o que inclui a busca de modelos. O sistema de modelo tenta carregar o modelo $httpe ele se torna uma "solicitação inesperada" para a simulação.

O que você precisa é uma maneira de pré-carregar os modelos $templateCachepara que eles já estejam disponíveis quando o Angular solicitar, sem usar $http.

A solução preferida: Karma

Se você estiver usando o Karma para executar seus testes (e deveria), você pode configurá-lo para carregar os modelos para você com o pré - processador ng-html2js . Ng-html2js lê os arquivos HTML que você especificar e os converte em um módulo Angular que pré-carrega o arquivo $templateCache.

Etapa 1: habilite e configure o pré-processador em seu 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 você estiver usando o Yeoman para montar seu aplicativo, essa configuração funcionará

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

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

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

Etapa 2: use o módulo em seus testes

// my-test.js

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

Para um exemplo completo, veja este exemplo canônico do guru de teste angular Vojta Jina . Inclui uma configuração inteira: configuração, modelos e testes do karma.

Uma Solução Não-Karma

Se você não usa o Karma por qualquer motivo (eu tive um processo de compilação inflexível no aplicativo herdado) e está apenas testando em um navegador, descobri que você pode contornar a aquisição do ngMock $httpBackendusando um XHR bruto para buscar o modelo de verdade e insira-o no $templateCache. Essa solução é muito menos flexível, mas faz o trabalho por enquanto.

// 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ério, no entanto. Use Karma . Demora um pouco de trabalho para configurar, mas permite executar todos os seus testes, em vários navegadores ao mesmo tempo, na linha de comando. Assim, você pode tê-lo como parte do seu sistema de integração contínua e / ou torná-lo uma tecla de atalho do seu editor. Muito melhor do que alt-tab-refresh-ad-infinitum.

SleepyMurph
fonte
6
Isso pode ser óbvio, mas se os outros ficam presos na mesma coisa e olhar aqui para respostas: Eu não poderia fazê-lo funcionar sem também adicionando o preprocessorspadrão de arquivo (por exemplo "path/to/templates/**/*.html") para a filesseção karma.conf.js.
18714 Johan Johan
1
Então, existem problemas importantes em não esperar pela resposta antes de continuar? Apenas atualizará o valor quando a solicitação voltar (o IE leva 30 segundos)?
Jackie
1
@ Jackie Suponho que você esteja falando sobre o exemplo "não-Karma", onde uso o falseparâmetro para a openchamada do XHR para torná-lo síncrono. Se você não fizer isso, a execução continuará alegremente e começará a executar seus testes, sem ter o modelo carregado. Isso leva você de volta ao mesmo problema: 1) A solicitação de modelo acaba. 2) O teste começa a executar. 3) O teste compila uma diretiva e o modelo ainda não está carregado. 4) Angular solicita o modelo por meio de seu $httpserviço, que é ridicularizado. 5) O $httpserviço simulado reclama: "solicitação inesperada".
precisa saber é o seguinte
1
Eu era capaz de correr grun-jasmim sem Karma.
FlavorScape 02/09/2014
5
Outra coisa: você precisa instalar o karma-ng-html2js-preprocessor ( npm install --save-dev karma-ng-html2js-preprocessor) e adicioná-lo à seção de plugins do seu karma.conf.js, de acordo com stackoverflow.com/a/19077966/859631 .
Vincent
37

O que acabei fazendo foi obter o cache do modelo e colocar a visualização lá. Eu não tenho controle sobre não usar o ngMock, ao que parece:

beforeEach(inject(function(_$rootScope_, _$compile_, $templateCache) {
    $scope = _$rootScope_;
    $compile = _$compile_;
    $templateCache.put('path/to/template.html', '<div>Here goes the template</div>');
}));
Jared
fonte
26
Aqui está minha reclamação com esse método ... Agora, se vamos ter um grande pedaço de html que vamos injetar como uma string no cache do modelo, o que faremos quando mudarmos o html no front end ? Alterar o html no teste também? IMO que é uma resposta insustentável e a razão pela qual seguimos usando a opção template over templateUrl. Embora eu não goste muito de ter meu html como uma string maciça na diretiva - é a solução mais sustentável para não precisar atualizar dois locais de html. O que não requer muita imagem que o html possa com o tempo não corresponder.
Sten Muchow
12

Este problema inicial pode ser resolvido adicionando o seguinte:

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

Isso ocorre porque ele tenta encontrar $ httpBackend no módulo ngMock por padrão e não está cheio.

bullgare
fonte
1
Bem, essa é a resposta correta para a pergunta original (essa foi a que me ajudou).
Mat
Tentei isso, mas passThrough () ainda não funcionou para mim. Ele ainda deu o erro "Solicitação inesperada".
frodo2975
8

A solução que cheguei precisa de jasmine-jquery.js e um servidor proxy.

Eu segui estes passos:

  1. No karma.conf:

adicione jasmine-jquery.js aos seus arquivos

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

adicione um servidor proxy que servirá seus equipamentos

proxies = {
    '/' : 'http://localhost:3502/'
};
  1. Nas suas especificações

    Descreva ('MySpec', function () {var $ scope, template; jasmine.getFixtures (). fixturesPath = 'public / parcials /'; // caminho personalizado para que você possa servir o modelo real usado no aplicativo 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. Execute um servidor no diretório raiz do seu aplicativo

    python -m SimpleHTTPServer 3502

  3. Execute karma.

Demorei um pouco para descobrir isso, tendo que pesquisar muitos posts, acho que a documentação sobre isso deve ser mais clara, pois é uma questão tão importante.

Tomas Romero
fonte
Eu estava com problemas para fornecer ativos localhost/base/specse adicionar um servidor proxy ao python -m SimpleHTTPServer 3502executá-lo. Senhor, você é um gênio!
Pbojinov 31/07/2013
Eu estava recebendo um elemento vazio retornado de $ compile em meus testes. Outros lugares sugeriram a execução de $ scope. $ Digest (): ainda vazia. A execução de $ scope. $ Apply () funcionou. Eu acho que foi porque eu estou usando um controlador na minha diretiva? Não tenho certeza. Obrigado pelo conselho! Ajudado!
Sam Simmons
7

Minha solução:

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

o teste:

'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
fonte
Primeira solução decente que não tenta forçar os desenvolvedores a usar o Karma. Por que caras angulares fazem algo tão ruim e facilmente evitável no meio de algo tão legal? pfff
Fabio Milheiro
Vejo você adicionar um 'test / mock / ** / *. Js' e suponho que seja para carregar todo o material zombado, como serviços e tudo? Estou procurando maneiras de evitar a duplicação de código de serviços simulados. Você poderia nos mostrar um pouco mais sobre isso?
Stephane
não me lembro exatamente, mas provavelmente havia configurações, por exemplo, JSONs para o serviço $ http. Nada chique.
bartek
Tive esse problema hoje - ótima solução. Nós usamos karma, mas também usamos Chutzpah - não há razão para sermos forçados a usar karma e apenas karma para podermos aplicar diretrizes de teste de unidade.
precisa saber é o seguinte
Estamos usando o Django com Angular, e isso funcionou como um encanto para testar uma diretiva que carrega seu templateUrl static, por exemplo, beforeEach(preloadTemplate(static_url +'seed/partials/beChartDropdown.html')); obrigado!
Aleck Landgraf
6

Se você estiver usando o Grunt, poderá usar modelos grunt-angulares. Ele carrega seus modelos no templateCache e é transparente à sua configuração de especificações.

Minha configuração de amostra:

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
fonte
6

Resolvi o mesmo problema de uma maneira um pouco diferente da solução escolhida.

  1. Primeiro, instalei e configurei o plug - in ng-html2js para o karma. No arquivo 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. Depois carreguei o módulo criado no beforeEach. No seu arquivo Spec.js:

    beforeEach(module('myApp', 'to/templates/myTemplate.html'));
  3. Então usei $ templateCache.get para armazená-lo em uma variável. No seu arquivo 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, eu testei desta maneira. No seu arquivo Spec.js:

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

Para carregar o html de modelo dinamicamente no $ templateCache, você pode usar o pré-processador de karma html2js, conforme explicado aqui

isso se resume a adicionar modelos ' .html' aos seus arquivos no arquivo conf.js. e também pré-processadores = {' .html': 'html2js'};

E use

beforeEach(module('..'));

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

no seu arquivo de teste js

Lior
fonte
Estou recebendoUncaught SyntaxError: Unexpected token <
Melbourne2991
2

se você estiver usando o Karma, considere usar o karma-ng-html2js-preprocessor para pré-compilar seus modelos HTML externos e evitar que o Angular tente obtê- los por HTTP durante a execução do teste. Eu lutei com isso por alguns dos nossos - no meu caso, os caminhos parciais do templateUrl resolvidos durante a execução normal do aplicativo, mas não durante os testes - devido a diferenças nas estruturas de aplicativo versus diretório de teste.

Nikita
fonte
2

Se você estiver usando o jasmine-maven-plugin junto com o RequireJS, poderá usar o plug - in de texto para carregar o conteúdo do modelo em uma variável e, em seguida, colocá-lo no cache do modelo.


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
fonte
Você pode fazer isso sem Karma?
Winnemucca 03/03
2

Se você usar requirejs em seus testes, poderá usar o plug-in 'text' para extrair o modelo html e colocá-lo no $ 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
fonte
0

Eu resolvo esse problema com a compilação de todos os modelos para o template templatecache. Estou usando gole, você pode encontrar uma solução semelhante para grunhido também. Meu templateUrls em diretivas, modais parece

`templateUrl: '/templates/directives/sidebar/tree.html'`
  1. Adicionar um novo pacote npm no meu package.json

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

  2. No arquivo gulp, adicione templatecache e uma nova tarefa:

    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. Adicione todos os arquivos js em index.html

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

  4. Aproveitar!

kitolog
fonte