Модульные тесты являются неотъемлемой частью разработки программного обеспечения, поскольку они помогают выпускать менее ошибочный код. Тестирование — это одна из нескольких вещей, которые нужно сделать, чтобы улучшить качество кода. AngularJS создан с целью тестирования, и любой код, написанный поверх фреймворка, может быть легко протестирован.
В моей последней статье о тестировании я рассказывал о модульном тестировании контроллеров, сервисов и провайдеров . Эта статья продолжает обсуждение тестирования с директивами. Директивы отличаются от других компонентов тем, что они используются не как объекты в коде JavaScript, а в HTML-шаблонах приложения. Мы пишем директивы для выполнения DOM-манипуляций и не можем игнорировать их в модульных тестах, поскольку они играют важную роль. Кроме того, они напрямую влияют на удобство использования приложения.
Я рекомендую вам проверить предыдущую статью о зависимостях Mocking в тестах AngularJS, так как мы будем использовать некоторые методы из этой статьи здесь. Если вы хотите поиграть с кодом, разработанным в этом руководстве, вы можете взглянуть на репозиторий GitHub, который я для вас настроил .
Директивы тестирования
Директивы являются наиболее важными и сложными компонентами AngularJS. Тестирование директив сложно, так как они не вызываются как функция. В приложениях директивы декларативно применяются к шаблону HTML. Их действия выполняются, когда шаблон компилируется и пользователь взаимодействует с директивой. При выполнении модульных тестов нам нужно автоматизировать действия пользователя и вручную скомпилировать HTML, чтобы проверить функциональность директив.
Настройка объектов для проверки директивы
Точно так же, как тестирование любой части логики на любом языке или использование любой платформы, нам нужно получить ссылки на объекты, необходимые перед началом тестирования директивы. Ключевым объектом, который будет создан здесь, является элемент, содержащий директиву, подлежащую проверке. Нам нужно скомпилировать фрагмент HTML с указанной в нем директивой, чтобы заставить директиву действовать. Например, рассмотрим следующую директиву:
angular.module('sampleDirectives', []).directive('firstDirective', function() { return function(scope, elem){ elem.append('<span>This span is appended from directive.</span>'); }; });
Будет запущен жизненный цикл директивы, и будут выполнены функции компиляции и компоновки. Мы можем вручную скомпилировать любой HTML-шаблон, используя сервис $compile
. Следующий блок beforeEach
компилирует beforeEach
выше директиву:
var compile, scope, directiveElem; beforeEach(function(){ module('sampleDirectives'); inject(function($compile, $rootScope){ compile = $compile; scope = $rootScope.$new(); }); directiveElem = getCompiledElement(); }); function getCompiledElement(){ var element = angular.element('<div first-directive></div>'); var compiledElement = compile(element)(scope); scope.$digest(); return compiledElement; }
При компиляции запускается жизненный цикл директивы. После следующего цикла дайджеста объект директивы будет в том же состоянии, в каком он отображается на странице.
Если директива зависит от какой-либо службы для достижения ее функциональности, эти службы должны быть проверены перед компиляцией директивы, чтобы в тестах можно было проверить вызовы любых методов службы. Мы увидим пример в следующем разделе.
Функция тестирования соединения
Функция Link является наиболее часто используемым свойством объекта определения директивы (DDO). Он содержит основную логику директивы. Эта логика включает в себя простые манипуляции с DOM, прослушивание событий pub / sub, наблюдение за изменением объекта или атрибута, вызов служб, обработку событий пользовательского интерфейса и так далее. Мы постараемся охватить большинство из этих сценариев.
DOM Manipulation
Давайте начнем со случая директивы, определенной в предыдущем разделе. Эта директива добавляет элемент span
к содержимому элемента, к которому применяется директива. Это можно проверить, найдя span
внутри директивы. Следующий тестовый пример подтверждает это поведение:
it('should have span element', function () { var spanElement = directiveElem.find('span'); expect(spanElement).toBeDefined(); expect(spanElement.text()).toEqual('This span is appended from directive.'); });
Зрителей
Поскольку директивы работают с текущим состоянием области, они должны иметь наблюдателей для обновления директивы при изменении состояния области. Модульный тест для наблюдателя должен манипулировать данными и принудительно запускать наблюдателя, вызывая $digest
и он должен проверять состояние директивы после цикла дайджеста.
Следующий код является слегка измененной версией вышеуказанной директивы. Он использует поле в области scope
чтобы связать текст внутри span
:
angular.module('sampleDirectives').directive('secondDirective', function(){ return function(scope, elem){ var spanElement = angular.element('<span>' + scope.text + '</span>'); elem.append(spanElement); scope.$watch('text', function(newVal, oldVal){ spanElement.text(newVal); }); }; });
Тестирование этой директивы аналогично первой директиве; за исключением того, что он должен быть проверен по данным о scope
и должен быть проверен на предмет обновления. Следующий тестовый пример проверяет, изменяется ли состояние директивы:
it('should have updated text in span', function () scope.text = 'some other text'; scope.$digest(); var spanElement = directiveElem.find('span'); expect(spanElement).toBeDefined(); expect(spanElement.text()).toEqual(scope.text); });
Та же самая техника может быть использована для проверки наблюдателей на атрибуты.
DOM События
Важность событий в любом приложении на основе пользовательского интерфейса заставляет нас убедиться, что они работают правильно. Одним из преимуществ приложений на основе JavaScript является то, что большая часть взаимодействия с пользователем тестируется через API. События могут быть протестированы с помощью API. Мы можем инициировать события, используя API jqLite и тестовую логику внутри события.
Рассмотрим следующую директиву:
angular.module('sampleDirectives').directive('thirdDirective', function () { return { template: '<button>Increment value!</button>', link: function (scope, elem) { elem.find('button').on('click', function(){ scope.value++; }); } }; });
Директива увеличивает value
свойства value
на единицу при каждом button
элемента button
. Контрольный пример для этой директивы должен инициировать событие click с помощью triggerHandler triggerHandler
а затем проверить, увеличивается ли значение. Вот как вы тестируете предыдущий код:
it('should increment value on click of button', function () { scope.value=10; var button = directiveElem.find('button'); button.triggerHandler('click'); scope.$digest(); expect(scope.value).toEqual(11); });
В дополнение к упомянутым здесь случаям функция связи содержит логику, включающую взаимодействие со службами или публикацию / подписку событий области действия. Чтобы проверить эти случаи, вы можете следовать методикам, рассмотренным в моем предыдущем посте. Те же самые методы могут быть применены и здесь.
Блок компиляции имеет обязанности, аналогичные ссылкам. Единственное отличие состоит в том, что блок компиляции не может использовать или манипулировать scope
, поскольку область не доступна во время выполнения компиляции. Обновления DOM, применяемые блоком компиляции, можно проверить, проверив HTML отображаемого элемента.
Шаблон директивы тестирования
Шаблон можно применить к директиве двумя способами: с помощью встроенного шаблона или с помощью файла. Мы можем проверить, применяется ли шаблон к директиве, а также есть ли в шаблоне определенные элементы или директивы.
Директиву со встроенным шаблоном проще проверить, поскольку она доступна в том же файле. Тестирование директивы с шаблоном, на который ссылаются из файла, довольно сложно, так как директива отправляет запрос $httpBackend
в templateUrl
. Добавление этого шаблона в $templateCache
облегчает задачу тестирования, и этим шаблоном будет легко поделиться. Это можно сделать с помощью задачи grunt-html2js grunt .
grunt-html2js
очень прост в настройке и использовании. Ему нужны исходный (ые) путь (и) html файла (ов) и целевой путь, куда должен быть записан результирующий скрипт. Ниже приведена конфигурация, использованная в примере кода:
html2js:{ main: { src: ['src/directives/*.html'], dest: 'src/directives/templates.js' } }
Теперь все, что нам нужно сделать, — это включить модуль, сгенерированный этой задачей, в наш код. По умолчанию имя модуля, сгенерированного grunt-html2js
является templates-main
но вы можете изменить его.
Рассмотрим следующую директиву:
angular.module('sampleDirectives', ['templates-main']) .directive('fourthDirective', function () { return { templateUrl: 'directives/sampleTemplate.html' }; });
И содержание шаблона:
<h3>Details of person {{person.name}}<h3> <another-directive></another-directive>
Шаблон имеет элемент another-directive
, который является другой директивой и является важной частью шаблона. Без директивы fourthDirective
anotherDirective
директива не будет работать fourthDirective
. Итак, мы должны проверить следующее после компиляции директивы:
- Если шаблон применяется внутри элемента директивы
- Если шаблон содержит элемент
another-directive
Это тесты для демонстрации этих случаев:
it('should applied template', function () { expect(directiveElem.html()).not.toEqual(''); }); it('should have another-person element', function () { expect(directiveElem.find('another-directive').length).toEqual(1); });
Вам не нужно писать тест для каждого отдельного элемента в шаблоне директивы. Если вы чувствуете, что определенный элемент или директива является обязательным в шаблоне, и без этого директива была бы неполной, добавьте тест для проверки существования такого компонента. При этом ваш тест будет жаловаться, если кто-то случайно удалит его.
Область применения директивы по испытаниям
Область действия директивы может быть одной из следующих:
- То же, что и объем окружающего элемента
- Наследуется от объема окружающего элемента
- Изолированная сфера
В первом случае вы можете не захотеть тестировать область, поскольку директива не должна изменять состояние области, когда она использует ту же область. Но в других случаях директива может добавить в область действия некоторые поля, которые определяют поведение директивы. Нам нужно проверить эти случаи.
Давайте рассмотрим пример директивы, использующей изолированную область видимости. Ниже приведена директива, которую мы должны проверить:
angular.module('sampleDirectives').directive('fifthDirective', function () { return { scope:{ config: '=', notify: '@', onChange:'&' } } }; })
В тестах этой директивы нам нужно проверить, все ли три свойства определены в изолированной области и назначены ли они с правильными значениями. В этом случае нам нужно протестировать следующие случаи:
- Свойство
config
для изолированной области должно быть таким же, как свойство в области видимости, и является двусторонним -
notify
свойство в изолированной области должно быть односторонним -
onChange
в изолированной области видимости должно быть функцией, и приonChange
должен вызываться метод области видимости.
Директива ожидает чего-то от окружающей области видимости, поэтому она требует немного другой настройки, и нам также нужно получить ссылку на изолированную область видимости.
Фрагмент ниже подготавливает область действия директивы и компилирует ее:
beforeEach(function() { module('sampleDirectives'); inject(function ($compile, $rootScope) { compile=$compile; scope=$rootScope.$new(); scope.config = { prop: 'value' }; scope.notify = true; scope.onChange = jasmine.createSpy('onChange'); }); directiveElem = getCompiledElement(); }); function getCompiledElement(){ var compiledDirective = compile(angular.element('<fifth-directive config="config" notify="notify" on-change="onChange()"></fifth-directive>'))(scope); scope.$digest(); return compiledDirective;
Теперь, когда у нас есть готовая директива, давайте проверим, назначена ли выделенная область с правильным набором свойств.
it('config on isolated scope should be two-way bound', function(){ var isolatedScope = directiveElem.isolateScope(); isolatedScope.config.prop = "value2"; expect(scope.config.prop).toEqual('value2'); }); it('notify on isolated scope should be one-way bound', function(){ var isolatedScope = directiveElem.isolateScope(); isolatedScope.notify = false; expect(scope.notify).toEqual(true); }); it('onChange should be a function', function(){ var isolatedScope = directiveElem.isolateScope(); expect(typeof(isolatedScope.onChange)).toEqual('function'); }); it('should call onChange method of scope when invoked from isolated scope', function () { var isolatedScope = directiveElem.isolateScope(); isolatedScope.onChange(); expect(scope.onChange).toHaveBeenCalled(); });
Требуется тестирование
Директива может строго или необязательно зависеть от одной или нескольких других директив. По этой причине у нас есть несколько интересных случаев для тестирования:
- Должен выдавать ошибку, если строго не указана директива
- Должен работать, если указана строго необходимая директива
- Не должен выдавать ошибку, если необязательная директива не указана
- Должен взаимодействовать с контроллером необязательной директивы, если он найден
Директива ниже требует ngModel
и опционально требует form
в родительском элементе:
angular.module('sampleDirectives').directive('sixthDirective', function () { return { require: ['ngModel', '^?form'], link: function(scope, elem, attrs, ctrls){ if(ctrls[1]){ ctrls[1].$setDirty(); } } }; });
Как видите, директива взаимодействует с контроллером form
только если она найдена. Хотя пример не имеет особого смысла, он дает представление о поведении. Тесты для этой директивы, охватывающие случаи, перечисленные выше, показаны ниже:
function getCompiledElement(template){ var compiledDirective = compile(angular.element(template))(scope); scope.$digest(); return compiledDirective; } it('should fail if ngModel is not specified', function () { expect(function(){ getCompiledElement('<input type="text" sixth-directive />'); }).toThrow(); }); it('should work if ng-model is specified and not wrapped in form', function () { expect(function(){ getCompiledElement('<div><input type="text" ng-model="name" sixth-directive /></div>'); }).not.toThrow(); }); it('should set form dirty', function () { var directiveElem = getCompiledElement('<form name="sampleForm"><input type="text" ng-model="name" sixth-directive /></form>'); expect(scope.sampleForm.$dirty).toEqual(true); });
Тестирование Заменить
Тестирование replace
очень просто. Нам просто нужно проверить, существует ли элемент директивы в скомпилированном шаблоне. Вот как вы это делаете:
//directive angular.module('sampleDirectives').directive('seventhDirective', function () { return { replace: true, template: '<div>Content in the directive</div>' }; }); //test it('should have replaced directive element', function () { var compiledDirective = compile(angular.element('<div><seventh-directive></seventh-directive></div>'))(scope); scope.$digest(); expect(compiledDirective.find('seventh-directive').length).toEqual(0); });
Тестирование Transclude
Transclusion имеет два случая: transclude установлено в true
и transclude установлено в элемент. Я не видел много случаев использования transclude, установленного в element, поэтому мы обсудим только случай, когда transclude установлено в true
.
Мы должны протестировать следующее, чтобы проверить, поддерживает ли директива трансклидированный контент:
- Если в шаблоне есть элемент с директивой
ng-transclude
- Если содержание сохраняется
Чтобы проверить директиву, нам нужно передать некоторое HTML-содержимое в директиву, которая будет скомпилирована, и затем проверить наличие вышеупомянутых случаев. Это директива, использующая transclude и ее тест:
//directive angular.module('sampleDirectives').directive('eighthDirective', function(){ return{ transclude: true, template:'<div>Text in the directive.<div ng-transclude></div></div>' }; }); //test it('should have an ng-transclude directive in it', function () { var transcludeElem = directiveElem.find('div[ng-transclude]'); expect(transcludeElem.length).toBe(1); }); it('should have transclude content', function () { expect(directiveElem.find('p').length).toEqual(1); });
Вывод
Как вы видели в этой статье, директивы сложнее тестировать по сравнению с другими концепциями в AngularJS. В то же время их нельзя игнорировать, поскольку они контролируют некоторые важные части приложения. Экосистема тестирования AngularJS облегчает нам тестирование любой части проекта. Я надеюсь, что благодаря этому руководству вы сможете более уверенно проверить свои директивы. Дайте мне знать ваши мысли в разделе комментариев.
Если вы хотите поиграть с кодом, разработанным в этом руководстве, вы можете взглянуть на репозиторий GitHub, который я для вас настроил .