Директива модульного тестирования AngularJS с templateUrl

122

У меня есть директива AngularJS с templateUrlопределенным файлом. Я пытаюсь провести модульное тестирование с помощью Jasmine.

Мой жасмин JavaScript выглядит следующим образом , в соответствии с рекомендацией этого :

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

        });
    });
});

Когда я запускаю это в своей ошибке спецификации Jasmine, я получаю следующую ошибку:

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

Все, что я хочу, - это загружать templateUrl как есть - я не хочу использовать respond. Я считаю, что это может быть связано с использованием ngMock вместо ngMockE2E . Если это виноват, как мне использовать последнее вместо первого?

Заранее спасибо!

Джаред
источник
1
Я не использовал .passThrough();таким образом, но, судя по документации, вы пробовали что-то вроде: $httpBackend.expectGET('path/to/template.html'); // do action here $httpBackend.flush();Я думаю, что это лучше подходит для вашего использования - вы не хотите перехватывать запрос, т.е. whenGet()вместо этого проверять, что он отправлен, а затем на самом деле Отправь это?
Alex Osborn
1
Спасибо за ответ. Не думаю, что expectGETотправляет запросы ... по крайней мере, из коробки. В документации/auth.py приводится их пример с $httpBackend.whenпрефиксом $httpBackend.expectGETи $httpBackend.flushвызовами.
Джаред
2
Это правильно, expectGetпросто проверяется, была ли попытка запроса.
Alex Osborn
1
Ах. Ну, мне нужен способ сказать $httpBackendмакету, чтобы он действительно использовал URL-адрес, указанный в директиве, templateUrlи получил его. Думал passThroughсделаю это. Вы знаете другой способ сделать это?
Джаред
2
Хм, я еще не много тестировал e2e, но проверял документы - пробовали ли вы вместо этого использовать бэкэнд e2e - я думаю, поэтому у вас нет метода passThrough - docs.angularjs.org/api/ngMockE2E.$httpBackend
Alex Osborn

Ответы:

187

Вы правы, что это связано с ngMock. Модуль ngMock автоматически загружается для каждого теста Angular и инициализирует макет $httpBackendдля обработки любого использования $httpслужбы, включая выборку шаблона. Система шаблонов пытается загрузить шаблон, $httpи это становится «неожиданным запросом» к макету.

Вам нужен способ предварительной загрузки шаблонов в, $templateCacheчтобы они уже были доступны, когда Angular их запросит, без использования $http.

Предпочтительное решение: карма

Если вы используете Karma для запуска своих тестов (а вы должны это делать), вы можете настроить его для загрузки шаблонов за вас с помощью препроцессора ng-html2js . Ng-html2js считывает указанные файлы HTML и преобразует их в модуль Angular, который предварительно загружает$templateCache .

Шаг 1. Включите и настройте препроцессор в вашем 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"
},

Если вы используете Yeoman для построения своего приложения, эта конфигурация будет работать

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

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

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

Шаг 2. Используйте модуль в своих тестах

// my-test.js

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

Чтобы получить полный пример, посмотрите этот канонический пример от гуру тестирования Angular Войты Джины. . Включает в себя всю настройку: конфиг кармы, шаблоны и тесты.

Решение без кармы

Если вы не используете Karma по какой-либо причине (у меня был негибкий процесс сборки в устаревшем приложении) и вы просто тестируете в браузере, я обнаружил, что вы можете обойти захват ngMock, $httpBackendиспользуя необработанный XHR для получения шаблона для реального и вставьте его в $templateCache. Это решение гораздо менее гибкое, но на данный момент оно выполняет свою работу.

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

Если серьезно. Используйте карму . Настройка требует небольшой работы, но позволяет запускать все тесты одновременно в нескольких браузерах из командной строки. Таким образом, вы можете использовать его как часть вашей системы непрерывной интеграции, и / или вы можете сделать его сочетанием клавиш в редакторе. Намного лучше, чем alt-tab-refresh-ad-infinitum.

SleepyMurph
источник
6
Это может быть очевидно, но если другие застрянут на одном и том же и будут искать здесь ответы: я не мог заставить его работать, не добавив также preprocessorsшаблон файла (например "path/to/templates/**/*.html") в filesраздел в karma.conf.js.
Johan
1
Есть ли какие-то серьезные проблемы с тем, чтобы не ждать ответа, прежде чем продолжить? Будет ли он просто обновлять значение, когда запрос вернется (IE занимает 30 секунд)?
Джеки
1
@Jackie Я предполагаю, что вы говорите о примере "не кармы", где я использую falseпараметр для openвызова XHR, чтобы сделать его синхронным. Если вы этого не сделаете, выполнение будет продолжено и начнется выполнение ваших тестов без загрузки шаблона. Это возвращает вас к той же проблеме: 1) Запрос на шаблон уходит. 2) Тест начинает выполняться. 3) Тест компилирует директиву, а шаблон все еще не загружен. 4) Angular запрашивает шаблон через свой $httpсервис, который имитируется. 5) Мок- $httpсервис жалуется: «неожиданный запрос».
SleepyMurph
1
Я смогла бежать без Кармы.
FlavorScape 02
5
Еще одна вещь: вам нужно установить karma-ng-html2js-preprocessor ( npm install --save-dev karma-ng-html2js-preprocessor) и добавить его в раздел плагинов вашего karma.conf.js, согласно stackoverflow.com/a/19077966/859631 .
Винсент
37

В итоге я получил кеш шаблонов и поместил туда представление. У меня нет контроля над тем, чтобы не использовать ngMock, оказывается:

beforeEach(inject(function(_$rootScope_, _$compile_, $templateCache) {
    $scope = _$rootScope_;
    $compile = _$compile_;
    $templateCache.put('path/to/template.html', '<div>Here goes the template</div>');
}));
Джаред
источник
26
Вот моя жалоба на этот метод ... Теперь, если у нас будет большой кусок html, который мы собираемся вставить в виде строки в кеш шаблонов, то что мы будем делать, когда мы изменим html на переднем конце ? Измените и html в тесте? ИМО, это неустойчивый ответ и причина, по которой мы использовали параметр template over templateUrl. Несмотря на то, что мне очень не нравится, когда мой html представляет собой массивную строку в директиве, это наиболее надежное решение, позволяющее избавиться от необходимости обновлять два места в html. Это не требует большого количества изображений, которые со временем могут не совпадать с HTML.
Стен Мучоу
12

Эту начальную проблему можно решить, добавив следующее:

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

Это потому, что он пытается найти $ httpBackend в модуле ngMock по умолчанию, а он не заполнен.

bullgare
источник
1
Что ж, это действительно правильный ответ на исходный вопрос (он мне помог).
Mat
Пробовал, но passThrough () все еще не работал у меня. Он по-прежнему выдавал ошибку «Неожиданный запрос».
frodo2975
8

Решение, которое я нашел, требует jasmine-jquery.js и прокси-сервера.

Я выполнил следующие шаги:

  1. В karma.conf:

добавьте jasmine-jquery.js в свои файлы

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

добавьте прокси-сервер, который будет обслуживать ваши светильники

proxies = {
    '/' : 'http://localhost:3502/'
};
  1. В вашей спецификации

    описать ('MySpec', function () {var $ scope, template; jasmine.getFixtures (). fixturesPath = 'public / partials /'; // настраиваемый путь, чтобы вы могли обслуживать реальный шаблон, который используете в приложении beforeEach (function () {шаблон = 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. Запустите сервер в корневом каталоге вашего приложения

    python -m SimpleHTTPServer 3502

  3. Запустите карму.

Мне потребовалось время, чтобы понять это, мне пришлось искать много сообщений, я думаю, что документация по этому поводу должна быть более ясной, поскольку это такой важный вопрос.

Томас Ромеро
источник
У меня возникли проблемы с обслуживанием ресурсов localhost/base/specsи добавлением прокси-сервера сpython -m SimpleHTTPServer 3502 исправленным запуском. Вы сэр гений!
pbojinov
В моих тестах $ compile возвращал пустой элемент. В других местах предлагалось запустить $ scope. $ Digest (): все еще пусто. Однако запуск $ scope. $ Apply () работал. Я думаю, это потому, что я использую контроллер в своей директиве? Точно сказать не могу. Спасибо за совет! Помогал!
Сэм Симмонс,
7

Мое решение:

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

тест:

'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');
  }));
});
Бартек
источник
Первое достойное решение, которое не пытается заставить разработчиков использовать Карму. Почему угловатые парни делают что-то настолько плохое, чего легко избежать, посреди чего-то такого крутого? pfff
Фабио Мильейро 01
Я вижу, вы добавляете «test / mock / ** / *. Js», и я полагаю, что он предназначен для загрузки всех имитируемых вещей, таких как службы и все такое? Я ищу способы избежать дублирования кода поддельных сервисов. Не могли бы вы рассказать нам об этом подробнее?
Стефан
точно не помню, но, вероятно, были настройки например JSON для службы $ http. Ничего особенного.
bartek
Была такая проблема сегодня - отличное решение. Мы используем карму, но мы также используем наглость - нет причин, по которым мы должны быть вынуждены использовать карму, и только карма, чтобы иметь возможность выполнять директивы модульного тестирования.
lwalden
Мы используем Django с Angular, и это сработало как прелесть, чтобы протестировать директиву, которая загружает свой templateUrl static, например, beforeEach(preloadTemplate(static_url +'seed/partials/beChartDropdown.html')); спасибо!
Aleck Landgraf
6

Если вы используете Grunt, вы можете использовать шаблоны grunt-angular. Он загружает ваши шаблоны в templateCache и прозрачен для вашей конфигурации спецификаций.

Мой образец конфигурации:

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

Я решил ту же проблему несколько иначе, чем выбранное решение.

  1. Сначала я установил и настроил плагин ng-html2js для кармы. В файле 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. Затем я загрузил модуль, созданный в beforeEach. В вашем файле Spec.js:

    beforeEach(module('myApp', 'to/templates/myTemplate.html'));
  3. Затем я использовал $ templateCache.get, чтобы сохранить его в переменной. В вашем файле 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. Наконец, я проверил это таким образом. В вашем файле Spec.js:

    describe('element', function() {
      it('should contain the template', function() {
        expect(element.html()).toMatch(template);
      });
    });
glepretre
источник
4

Чтобы загрузить шаблон html динамически в $ templateCache, вы можете просто использовать препроцессор html2js karma, как описано здесь.

это сводится к добавлению шаблонов ' .html' в ваши файлы в файле conf.js, а также preprocessors = {' .html': 'html2js'};

и использовать

beforeEach(module('..'));

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

в ваш файл тестирования js

Лиор
источник
Я получаюUncaught SyntaxError: Unexpected token <
Melbourne2991
2

если вы используете Карму, подумайте об использовании karma-ng-html2js-preprocessor для предварительной компиляции ваших внешних HTML-шаблонов и избегайте того, чтобы Angular пытался HTTP GET во время выполнения теста. Я боролся с этим в течение нескольких наших - в моем случае частичные пути templateUrl разрешались во время обычного выполнения приложения, но не во время тестов - из-за различий в структурах приложений и тестовых каталогов.

Никита
источник
2

Если вы используете jasmine-maven-plugin вместе с RequireJS, вы можете использовать текстовый плагин для загрузки содержимого шаблона в переменную, а затем поместить его в кеш шаблона.


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

    });
});
Леонард Брюнингс
источник
Сможете ли вы сделать это без кармы?
Winnemucca
2

Если вы используете requirejs в своих тестах, вы можете использовать плагин 'text' для извлечения шаблона html и помещения его в $ 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

Я решил эту проблему, скомпилировав все шаблоны в templatecache. Я использую gulp, вы можете найти подобное решение и для grunt. Мои templateUrls в директивах, модальные окна выглядят как

`templateUrl: '/templates/directives/sidebar/tree.html'`
  1. Добавьте новый пакет npm в мой package.json

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

  2. В файле gulp добавьте templatecache и новую задачу:

    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. Добавьте все js файлы в index.html

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

  4. Наслаждайтесь!

kitolog
источник