如何在AngularJS中对隔离范围指令进行单元测试

时间:2013-06-28 19:01:48

标签: javascript unit-testing angularjs jasmine angularjs-directive

在AngularJS中单独测试隔离范围的好方法是什么

JSFiddle showing unit test

指令摘录

    scope: {name: '=myGreet'},
    link: function (scope, element, attrs) {
        //show the initial state
        greet(element, scope[attrs.myGreet]);

        //listen for changes in the model
        scope.$watch(attrs.myGreet, function (name) {
            greet(element, name);
        });
    }

我想确保指令正在监听更改 - 使用隔离范围:

    it('should watch for changes in the model', function () {
        var elm;
        //arrange
        spyOn(scope, '$watch');
        //act
        elm = compile(validHTML)(scope);
        //assert
        expect(scope.$watch.callCount).toBe(1);
        expect(scope.$watch).toHaveBeenCalledWith('name', jasmine.any(Function));
    });

更新 我通过检查预期的观察者是否被添加到子范围来实现它,但它非常脆弱,并且可能以未记录的方式使用访问者(也可能在没有通知的情况下进行更改!)。

//this is super brittle, is there a better way!?
elm = compile(validHTML)(scope);
expect(elm.scope().$$watchers[0].exp).toBe('name');

更新2: 正如我所提到的,这很脆弱!这个想法仍然有效,但在较新版本的AngularJS中,访问者已从scope()更改为isolateScope()

//this is STILL super brittle, is there a better way!?
elm = compile(validHTML)(scope);                       
expect(elm.isolateScope().$$watchers[0].exp).toBe('name');

4 个答案:

答案 0 :(得分:101)

angular element api docs。如果使用 element.scope(),则会获得在指令的scope属性中定义的元素范围。如果使用 element.isolateScope(),则会获得整个隔离范围。 例如,如果你的指令看起来像这样:

scope : {
 myScopeThingy : '='
},
controller : function($scope){
 $scope.myIsolatedThingy = 'some value';
}

然后在测试中调用element.scope()将返回

{ myScopeThingy : 'whatever value this is bound to' }

但如果你调用element.isolateScope(),你就会得到

{ 
  myScopeThingy : 'whatever value this is bound to', 
  myIsolatedThingy : 'some value'
}

从角度1.2.2或1.2.3开始,确实无法确定。 在以前的版本中,您只有element.scope()。

答案 1 :(得分:11)

您可以var isolateScope = myDirectiveElement.scope()来获取隔离范围。

你真的不需要测试那个$ watch被调用了..这比测试你的app更多测试angularjs。但我想这只是问题的一个例子。

答案 2 :(得分:1)

将逻辑移动到单独的控制器,即:

//will get your isolate scope
function MyCtrl($scope)
{
  //non-DOM manipulating ctrl logic here
}
app.controller(MyCtrl);

function MyDirective()
{
  return {
    scope     : {},
    controller: MyCtrl,
    link      : function (scope, element, attrs)
    {
      //moved non-DOM manipulating logic to ctrl
    }
  }
}
app.directive('myDirective', MyDirective);

并像对待任何控制器一样测试后者 - 直接传递范围对象(参见控制器 section here)。

如果您需要在测试中触发$ watch,请执行以下操作:

describe('MyCtrl test', function ()
{
  var $rootScope, $controller, $scope;

  beforeEach(function ()
  {
    inject(function (_$rootScope_, _$controller_)
    {
      // The injector unwraps the underscores (_) from around the parameter names when matching
      $rootScope = _$rootScope_;
      $controller = _$controller_;
    });

    $scope = $rootScope.$new({});
    $scope.foo = {x: 1}; //initial scope state as desired
    $controller(MyCtrl, {$scope: $scope}); //or by name as 'MyCtrl'
  });

  it('test scope property altered on $digest', function ()
  {
    $scope.$digest(); //trigger $watch
    expect($scope.foo.x).toEqual(1); //or whatever
  });
});

答案 3 :(得分:0)

我不确定隔离范围是否可行(尽管我希望有人证明我错了)。在指令中创建的隔离范围是隔离的,因此指令中的$ watch方法与您在单元测试中监视的范围不同。如果将scope:{}更改为scope:true,则指令范围将继承原型并且您的测试应该通过。

我想这不是最理想的解决方案,因为有时(很多时候),隔离范围是一件好事。