Ric*_*ral 17 dom unit-testing angularjs angularjs-directive
在问我真正的问题之前,我有一个不同的... 在Angular指令中单元测试 DOM操作是否有意义?
例如,这是我的完整链接功能:
function linkFn(scope, element) {
    var ribbon = element[0];
    var nav = ribbon.children[0];
    scope.ctrl.ribbonItemClick = function (index) {
        var itemOffsetLeft;
        var itemOffsetRight;
        var item;
        if (scope.ctrl.model.selectedIndex === index) {
            return;
        }
        scope.ctrl.model.selectedIndex = index;
        item = nav.querySelectorAll('.item')[index];
        itemOffsetLeft = item.offsetLeft - ribbon.offsetLeft;
        itemOffsetRight = itemOffsetLeft + item.clientWidth;
        if (itemOffsetLeft < nav.scrollLeft) {
            nav.scrollLeft = itemOffsetLeft - MAGIC_PADDING;
        }
        if(itemOffsetRight > nav.clientWidth + nav.scrollLeft) {
            nav.scrollLeft = itemOffsetRight - nav.clientWidth + MAGIC_PADDING;
        }
        this.itemClick({
            item: scope.ctrl.model.items[index],
            index: index
        });
        $location.path(scope.ctrl.model.items[index].href);
    };
    $timeout(function $timeout() {
        var item = nav.querySelector('.item.selected');
        nav.scrollLeft = item.offsetLeft - ribbon.offsetLeft - MAGIC_PADDING;
    });
}
这是一个可滚动的选项卡式组件,我不知道如何测试3个实例nav.scrollLeft = x.
if当单击一个项目(仅部分可见)时,会发生前两个语句.左/右(每个if)项目将捕捉到组件的左/右边框.
第三个是,如果在加载组件时它不可见,则将所选项目置于视图中.
我如何用Karma/Jasmine进行单元测试.这样做是否有意义,或者我应该使用Protractor进行功能测试?
use*_*490 13
测试指令时,查找设置或返回显式值的内容.这些通常很容易断言,用Jasmine和Karma对它们进行单元测试是有意义的.
看一下Angular的测试ng-src.在这里,他们通过声明src元素上的属性设置为正确的值来测试该指令是否有效.它是显式的:要么src属性具有特定值,要么不具有特定值.
it('should not result empty string in img src', inject(function($rootScope, $compile) {
  $rootScope.image = {};
  element = $compile('<img ng-src="{{image.url}}">')($rootScope);
  $rootScope.$digest();
  expect(element.attr('src')).not.toBe('');
  expect(element.attr('src')).toBe(undefined);
}));
与ng-bind相同.在这里,他们将一串HTML传递给$ compiler,然后断言返回值的HTML填充了实际的范围值.再次,它是明确的.
it('should set text', inject(function($rootScope, $compile) {
  element = $compile('<div ng-bind="a"></div>')($rootScope);
  expect(element.text()).toEqual('');
  $rootScope.a = 'misko';
  $rootScope.$digest();
  expect(element.hasClass('ng-binding')).toEqual(true);
  expect(element.text()).toEqual('misko');
}));
当您进入更复杂的场景,例如针对视口可见性进行测试或测试特定元素是否位于页面上的正确位置时,您可以尝试测试CSS和style属性是否正确设置,但这种方式非常快速且不建议使用.此时你应该看看Protractor或类似的e2e测试工具.
小智 7
我会100%想要测试你的指令的所有路径,即使它不是最容易的事情.但是,您可以采取一些方法来简化此过程.
对我而言最突出的是关于设置导航的复杂逻辑scrollLeft.为什么不把它分解成一个单独的服务,可以自己进行单元测试?
app.factory('AutoNavScroller', function() {
  var MAGIC_PADDING;
  MAGIC_PADDING = 25;
  return function(extraOffsetLeft) {
    this.getScrollPosition = function(item, nav) {
      var itemOffsetLeft, itemOffsetRight;
      itemOffsetLeft = item.offsetLeft - extraOffsetLeft;
      itemOffsetRight = itemOffsetLeft + item.clientWidth;
      if ( !!nav && itemOffsetRight > nav.clientWidth + nav.scrollLeft) {
        return itemOffsetRight - nav.clientWidth + MAGIC_PADDING;
      } else {
        return itemOffsetLeft - MAGIC_PADDING;
      }
    };
  }
});
这样可以更容易地测试所有路径和重构(您可以看到我能够在上面进行测试.测试可以在下面看到:
describe('AutoNavScroller', function() {
  var AutoNavScroller;
  beforeEach(module('app'));
  beforeEach(inject(function(_AutoNavScroller_) {
    AutoNavScroller = _AutoNavScroller_;
  }));
  describe('#getScrollPosition', function() {
    var scroller, item;
    function getScrollPosition(nav) {
      return scroller.getScrollPosition(item, nav);
    }
    beforeEach(function() {
      scroller = new AutoNavScroller(50);
      item = {
        offsetLeft: 100
      };
    })
    describe('with setting initial position', function() {
      it('gets the initial scroll position', function() {
        expect(getScrollPosition()).toEqual(25);
      });
    });
    describe('with item offset left of the nav scroll left', function() {
      it('gets the scroll position', function() {
        expect(getScrollPosition({
          scrollLeft: 100
        })).toEqual(25);
      });
    });
    describe('with item offset right of the nav width and scroll left', function() {
      beforeEach(function() {
        item.clientWidth = 300;
      });
      it('gets the scroll position', function() {
        expect(getScrollPosition({
          scrollLeft: 25,
          clientWidth: 50
        })).toEqual(325);
      });
    });
  });
});
现在我们已经打破了我们的指令,我们可以注入服务并确保它被正确调用.
app.directive('ribbonNav', function(AutoNavScroller, $timeout) {
  return {
    link: function(scope, element) {
      var navScroller;
      var ribbon = element[0];
      var nav = ribbon.children[0];
      // Assuming ribbon offsetLeft remains the same
      navScroller = new AutoNavScroller(ribbon.offsetLeft);
      scope.ctrl.ribbonItemClick = function (index) {
        if (scope.ctrl.model.selectedIndex === index) {
            return;
        }
        scope.ctrl.model.selectedIndex = index;
        item = nav.querySelectorAll('.item')[index];
        nav.scrollLeft = navScroller.getScrollLeft(item, nav);
        // ...rest of directive
      };
      $timeout(function $timeout() {
        var item = nav.querySelector('.item.selected');
        // Sets initial nav scroll left
        nav.scrollLeft = navScroller.getScrollLeft(item);
      });
    }
  }
});
确保我们的指令继续使用该服务的最简单方法是监视它将调用的方法,并确保它们接收正确的参数:
describe('ribbonNav', function() {
  var $compile, $el, $scope, AutoNavScroller;
  function createRibbonNav() {
    $el = $compile($el)($scope);
    angular.element(document)
    $scope.$digest();
    document.body.appendChild($el[0]);
  }
  beforeEach(module('app'));
  beforeEach(module(function ($provide) {
    AutoNavScroller = jasmine.createSpy();
    AutoNavScroller.prototype.getScrollLeft = function(item, nav) {
      return !nav ? 50 : 100;
    };
    spyOn(AutoNavScroller.prototype, 'getScrollLeft').and.callThrough();
    $provide.provider('AutoNavScroller', function () {
      this.$get = function () {
        return AutoNavScroller;
      }
    });
  }));
  beforeEach(inject(function(_$compile_, $rootScope) {
    $compile = _$compile_;
    $el = "<div id='ribbon_nav' ribbon-nav><div style='width:50px;overflow:scroll;float:left;'><div class='item selected' style='height:100px;width:200px;float:left;'>An Item</div><div class='item' style='height:100px;width:200px;float:left;'>An Item</div></div></div>";
    $scope = $rootScope.$new()
    $scope.ctrl = {
      model: {
        selectedIndex: 0
      }
    };
    createRibbonNav();
  }));
  afterEach(function() {
    document.getElementById('ribbon_nav').remove();
  });
  describe('on link', function() {
    it('calls AutoNavScroller with selected item', inject(function($timeout) {
      expect(AutoNavScroller).toHaveBeenCalledWith(0);
    }));
    it('calls AutoNavScroller with selected item', inject(function($timeout) {
      $timeout.flush();
      expect(AutoNavScroller.prototype.getScrollLeft)
        .toHaveBeenCalledWith($el[0].children[0].children[0]);
    }));
    it('sets the initial nav scrollLeft', inject(function($timeout) {
      $timeout.flush();
      expect($el[0].children[0].scrollLeft).toEqual(50);
    }));
  });
  describe('ribbonItemClick', function() {
    beforeEach(function() {
      $scope.ctrl.ribbonItemClick(1);
    });
    it('calls AutoNavScroller with item', inject(function($timeout) {
      expect(AutoNavScroller.prototype.getScrollLeft)
        .toHaveBeenCalledWith($el[0].children[0].children[1], $el[0].children[0]);
    }));
    it('sets the nav scrollLeft', function() {
      expect($el[0].children[0].scrollLeft).toEqual(100);
    });
  });
});
现在,显然这些规格可以通过100种方式进行重构,但是你可以看到,一旦我们开始打破复杂的逻辑,更高的覆盖范围就更容易实现.围绕模拟对象存在一些风险,因为它可能会使您的测试变得脆弱,但我相信这里的权衡是值得的.另外,我可以肯定地看到它AutoNavScroller被推广并在其他地方重复使用.如果代码之前存在于代码中,那就不可能实现.
无论如何,我认为Angular很棒的原因是能够测试这些指令以及它们如何与DOM交互.这些茉莉花规格可以在任何浏览器中运行,并会很快出现不一致或回归.
此外,这里有一个plunkr,所以你可以看到所有移动的部分和实验:http://plnkr.co/edit/wvj4TmmJtxTG0KW7v9rn?p = preview
| 归档时间: | 
 | 
| 查看次数: | 6800 次 | 
| 最近记录: |