angularjs指令
单元测试是软件开发中必不可少的部分,因为它们可以帮助您释放更少的错误代码。 测试是提高代码质量必须要做的几件事之一。 创建AngularJS时要考虑到测试,并且可以轻松测试在框架顶部编写的任何代码。
在上一篇有关测试的文章中, 我介绍了单元测试控制器,服务和提供者 。 本文继续讨论使用指令进行测试。 指令与其他组件不同,因为它们在JavaScript代码中不用作对象,而在应用程序HTML模板中用作对象。 我们编写指令来执行DOM操作,在单元测试中我们不能忽略它们,因为它们起着重要的作用。 此外,它们直接影响应用程序的可用性。
我鼓励您阅读过去有关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>');
};
});
指令的生命周期将被执行,并且编译和链接功能将被执行。 我们可以使用$compile
服务手动编译任何HTML模板。 以下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;
}
在编译时,将启动指令的生命周期。在下一个摘要循环之后,指令对象将处于与页面上相同的状态。
如果指令依赖于任何服务来实现其功能,则在编译指令之前必须对这些服务进行模拟,以便可以在测试中检查对任何服务方法的调用。 我们将在下一部分中看到一个示例。
测试链接功能
链接功能是指令定义对象(DDO)最常用的属性。 它包含了指令的大多数核心逻辑。 该逻辑包括简单的DOM操作,侦听发布/订阅事件,监视对象或属性的更改,调用服务,处理UI事件,等等。 我们将尝试涵盖大多数情况。
DOM操作
让我们从上一节中定义的指令开始。 该指令将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事件
在任何基于UI的应用程序中,事件的重要性迫使我们确保事件能够正常运行。 基于JavaScript的应用程序的优点之一是,大多数用户交互都可以通过API进行测试。 可以使用API测试事件。 我们可以使用jqLite API触发事件并在事件内部测试逻辑。
考虑以下指令:
angular.module('sampleDirectives').directive('thirdDirective', function () {
return {
template: '<button>Increment value!</button>',
link: function (scope, elem) {
elem.find('button').on('click', function(){
scope.value++;
});
}
};
});
该指令在每次单击button
元素时将value
属性的value
增加一。 该指令的测试用例必须使用jqLite的triggerHandler
触发click事件,然后检查该值是否增加。 这是测试前面的代码的方式:
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
,因为在编译运行时范围不可用。 可以通过检查渲染元素HTML来测试由compile块应用的DOM更新。
测试指令的模板
免费学习PHP!
全面介绍PHP和MySQL,从而实现服务器端编程的飞跃。
原价$ 11.95 您的完全免费
可以通过两种方式将模板应用于指令:使用内联模板或使用文件。 我们可以验证模板是否应用于指令,以及模板中是否包含某些元素或指令。
具有内联模板的指令更容易测试,因为它在同一文件中可用。 使用从文件引用的模板来测试指令很棘手,因为该指令向templateUrl
发出$httpBackend
请求。 将此模板添加到$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
元素,这是另一个指令,它是模板的重要组成部分。 如果没有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
属性应与范围上的config
属性相同,并且是双向绑定的 - 隔离范围上的
notify
属性应为单向绑定 - 隔离范围内的
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设置为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存储库 。
翻译自: https://www.sitepoint.com/angular-testing-tips-testing-directives/
angularjs指令