Как вы издеваетесь над директивами, чтобы включить модульное тестирование директивы более высокого уровня?
В нашем приложении у нас есть несколько уровней вложенных директив. Я пытаюсь написать некоторые модульные тесты для директив верхнего уровня. Я издевался над тем, что сама директива требует, но теперь я сталкиваюсь с ошибками директив нижнего уровня. В моих модульных тестах для директивы верхнего уровня я не хочу беспокоиться о том, что происходит в директивах нижнего уровня. Я просто хочу высмеять директиву нижнего уровня и в основном не делаю ничего, поэтому я могу тестировать директиву верхнего уровня в изоляции.
Я попробовал переписать определение директивы, выполнив что-то вроде этого:
angular.module("myModule").directive("myLowerLevelDirective", function() {
return {
link: function(scope, element, attrs) {
//do nothing
}
}
});
Однако это не перезаписывает его, оно просто запускает это в дополнение к реальной директиве. Как я могу остановить эти директивы нижнего уровня от каких-либо действий в моем unit test для директивы верхнего уровня?
Ответы
Ответ 1
Из-за реализации регистрации директивы не представляется возможным заменить существующую дирекцию на насмешку.
Однако у вас есть несколько способов unit test вашей директивы более высокого уровня без помех от директив нижнего уровня:
1) Не используйте директиву нижнего уровня в шаблоне unit test:
Если ваша директива более низкого уровня не добавлена директивой более высокого уровня, в unit test используйте шаблон с только директивой более высокого уровня:
var html = "<div my-higher-level-directive></div>";
$compile(html)(scope);
Таким образом, директива нижнего уровня не будет мешать.
2) Используйте службу в своей директивной реализации:
Вы можете обеспечить функцию ссылки на директиву нижнего уровня службой:
angular.module("myModule").directive("myLowerLevelDirective", function(myService) {
return {
link: myService.lowerLevelDirectiveLinkingFunction
}
});
Затем вы можете высмеять эту службу в своем unit test, чтобы избежать вмешательства в вашу директиву более высокого уровня. Эта услуга может даже обеспечить весь объект директивы, если это необходимо.
3) Вы можете перезаписать директиву нижнего уровня с помощью директивы терминала:
angular.module("myModule").directive("myLowerLevelDirective", function(myService) {
return {
priority: 100000,
terminal: true,
link: function() {
// do nothing
}
}
});
Если опция терминала и более высокий приоритет, ваша реальная директива нижнего уровня не будет выполнена. Дополнительная информация в директивном документе .
Посмотрите, как это работает в этом Plunker.
Ответ 2
Директивы - это просто фабрики, поэтому лучший способ сделать это - издеваться над factory директивы при использовании функции module
, как правило, в блоке beforeEach
. Предполагая, что у вас есть директива с именем do-something, используемая директивой, называемой do-something-else, вы бы издевались над ней как таковой:
beforeEach(module('yourapp/test', function($provide){
$provide.factory('doSomethingDirective', function(){ return {}; });
}));
// Or using the shorthand sytax
beforeEach(module('yourapp/test', { doSomethingDirective: {} ));
Тогда директива будет переопределена, когда шаблон будет скомпилирован в вашем тесте
inject(function($compile, $rootScope){
$compile('<do-something-else></do-something-else>', $rootScope.$new());
});
Обратите внимание, что вам нужно добавить суффикс 'Directive' для имени, потому что компилятор делает это внутри: https://github.com/angular/angular.js/blob/821ed310a75719765448e8b15e3a56f0389107a5/src/ng/compile.js#L530
Ответ 3
Чистым способом насмешки над директивой является $compileProvider
beforeEach(module('plunker', function($compileProvider){
$compileProvider.directive('d1', function(){
var def = {
priority: 100,
terminal: true,
restrict:'EAC',
template:'<div class="mock">this is a mock</div>',
};
return def;
});
}));
Вы должны убедиться, что макет получает более высокий приоритет, чем директива, которую вы издеваетесь, и что макет является терминальным, так что исходная директива не будет скомпилирована.
priority: 100,
terminal: true,
Результат будет выглядеть следующим образом:
Учитывая эту директиву:
var app = angular.module('plunker', []);
app.directive('d1', function(){
var def = {
restrict: 'E',
template:'<div class="d1"> d1 </div>'
}
return def;
});
Вы можете издеваться над этим следующим образом:
describe('testing with a mock', function() {
var $scope = null;
var el = null;
beforeEach(module('plunker', function($compileProvider){
$compileProvider.directive('d1', function(){
var def = {
priority: 9999,
terminal: true,
restrict:'EAC',
template:'<div class="mock">this is a mock</div>',
};
return def;
});
}));
beforeEach(inject(function($rootScope, $compile) {
$scope = $rootScope.$new();
el = $compile('<div><d1></div>')($scope);
}));
it('should contain mocked element', function() {
expect(el.find('.mock').length).toBe(1);
});
});
Еще несколько вещей:
-
Когда вы создаете свой макет, вам нужно подумать, нужно ли вам replace:true
и/или template
. Например, если вы издеваетесь ng-src
, чтобы предотвратить вызовы на бэкэнд, вам не нужно replace:true
, и вы не хотите указывать template
. Но если вы издеваетесь над чем-то визуальным, вы можете захотеть.
-
Если вы установите приоритет выше 100, ваши атрибуты mocks не будут интерполированы. См. $компилировать исходный код. Например, если вы издеваетесь ng-src
и устанавливаете priority:101
, то вы получите ng-src="{{variable}}"
not ng-src="interpolated-value"
в своем макете.
Вот плункер со всем. Спасибо @trodrigues за то, что указали мне в правильном направлении.
Вот несколько документов, которые объясняют больше, проверьте раздел "Блоки конфигурации". Благодаря @ebelanger!
Ответ 4
Вы можете изменить свои шаблоны внутри $templateCache
, чтобы удалить любые директивы нижнего уровня:
beforeEach(angular.mock.inject(function ($templateCache) {
$templateCache.put('path/to/template.html', '<div></div>');
}));
Ответ 5
Будучи вынужденным думать об этом больше, я придумал решение, которое наполняет наши потребности. Все наши директивы являются атрибутами, поэтому я создал директиву attributeRemover
для использования во время модульных тестов. Это выглядит примерно так:
angular.module("myModule").directive("attributeRemover", function() {
return {
priority: -1, //make sure this runs last
compile: function(element, attrs) {
var attributesToRemove = attrs.attributeRemover.split(",");
angular.forEach(attributesToRemove, function(currAttributeToRemove) {
element.find("div[" + currAttributeToRemove + "]").removeAttr(currAttributeToRemove);
});
}
}
});
Затем html для проверяемой директивы выглядит примерно так:
<div my-higher-level-directive attribute-remover="my-lower-level-directive,another-loweler-level-directive"></div>
Итак, когда my-higher-level-directive
скомпилируется, attribute-remover
уже удалит атрибуты для директив нижнего уровня, и поэтому мне не нужно беспокоиться о том, что они делают.
Вероятно, существует более надежный способ сделать это для всех видов директив (а не только для атрибутов), и я не уверен, что это работает, если использовать только встроенный JQLite, но он работает для того, что нам нужно.
Ответ 6
Loved Sylvain answer настолько, что мне пришлось превратить его в вспомогательную функцию. Чаще всего мне нужно убить дочернюю директиву, чтобы я мог скомпилировать и протестировать директиву родительского контейнера без его зависимостей. Таким образом, этот помощник позволяет нам сделать это:
function killDirective(directiveName) {
angular.mock.module(function($compileProvider) {
$compileProvider.directive(directiveName, function() {
return {
priority: 9999999,
terminal: true
}
});
});
}
С этим вы можете полностью отключить директиву, запустив это до создания инжектора:
killDirective('myLowerLevelDirective');
Ответ 7
Вот еще одна небольшая идея. Просто введите этот код в помощники жасмина (кофе script)
window.mockDirective = (name, factoryFunction) ->
mockModule = angular.module('mocks.directives', ['ng'])
mockModule.directive(name, factoryFunction)
module ($provide) ->
factoryObject = angular.injector([mockModule.name]).get("#{name}Directive")
$provide.factory "#{name}Directive", -> factoryObject
null
И используйте его:
beforeEach mockDirective, "myLowerLevelDirective", ->
link: (scope, element) ->
Это полностью удалит все другие реализации данной директивы, предоставив полный доступ к проверке переданных аргументов директиве. Например, директива mm.foundation alert может быть изделена с помощью:
beforeEach mockDirective 'alert', ->
scope:
type: '='
а затем протестирован:
expect(element.find('alert').data('$isolateScopeNoTemplate').type).toEqual