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​​测试事件。 我们可以使用jqLit​​e 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增加一。 该指令的测试用例必须使用jqLit​​e的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更新。

测试指令的模板

可以通过两种方式将模板应用于指令:使用内联模板或使用文件。 我们可以验证模板是否应用于指令,以及模板中是否包含某些元素或指令。

具有内联模板的指令更容易测试,因为它在同一文件中可用。 使用从文件引用的模板来测试指令很棘手,因为该指令向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预期将无法正常工作。 因此,在编译指令后,我们必须验证以下内容:

  1. 如果模板在指令元素内应用
  2. 如果模板包含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);
});

您无需为指令模板中的每个元素编写测试。 如果您认为模板中必须包含某个元素或指令,并且没有该指令将不完整,请添加测试以检查该组件是否存在。 这样做,如果有人不小心将其删除,您的测试就会抱怨。

测试指令的范围

指令的范围可以是以下之一:

  1. 与周围元素的范围相同
  2. 继承自周围元素的范围
  3. 孤立范围

在第一种情况下,您可能不希望测试范围,因为当指令使用相同的范围时,不应修改该范围的状态。 但是在其他情况下,伪指令可能会在作用域中添加一些字段来驱动伪指令的行为。 我们需要测试这些情况。

让我们以使用隔离范围的指令为例。 以下是我们必须测试的指令:

angular.module('sampleDirectives').directive('fifthDirective', function () {
  return {
    scope:{
      config: '=',
      notify: '@',
      onChange:'&'
    }
  }
};
})

在此指令的测试中,我们需要检查隔离的范围是否已定义所有三个属性,以及是否为它们分配了正确的值。 在这种情况下,我们需要测试以下情况:

  1. 隔离作用域上的config属性应与作用域上的config属性相同,并且是双向绑定的
  2. 隔离范围上的notify属性应为单向绑定
  3. 隔离范围上的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();
});

测试要求

指令可以严格或可选地依赖一个或一组其他指令。 因此,我们需要测试一些有趣的案例:

  1. 如果未指定严格要求的指令,则应引发错误
  2. 如果指定了严格要求的指令,则应该可以使用
  3. 如果未指定可选的指令,则不应引发错误
  4. 如果找到,则应与可选指令的控制器进行交互

下面的指令需要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

我们必须测试以下内容,以检查指令是否支持已包含的内容:

  1. 如果模板上带有ng-transclude指令的元素
  2. 如果内容被保留

为了测试指令,我们需要在指令内部传递一些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存储库

From: https://www.sitepoint.com/angular-testing-tips-testing-directives/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值