Как провести модульное тестирование изолированной директивы области в AngularJS

81

Каков хороший способ модульного тестирования изолированной области в AngularJS

JSFiddle, показывающий модульный тест

Фрагмент директивы

    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');
Daniellmb
источник
Вы нашли способ настроить слежку?
tusharmath
@Tushar не совсем так, как раньше, есть способ заставить его работать, но он может быть изменен без предварительного уведомления, поэтому используйте его на свой страх и риск.
daniellmb

Ответы:

102

См. Документацию по api angular . Если вы используете 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'
}

Это верно для angular 1.2.2 или 1.2.3, не уверен точно. В предыдущих версиях у вас был только element.scope ().

Яир Тавор
источник
1
v1.2.3 feat (jqLite): выставляем метод получения изолятэскопе () аналогично scope () github.com/angular/angular.js/commit/…
daniellmb 03
1
но где вы следите за методом $ watch?
tusharmath
1
вы можете раскрыть функцию, которая запускается на $ watch, а затем следить за ней. В директиве установите «scope.myfunc = function () ...», затем в $ watch сделайте «$ scope. $ Watch ('myName', scope.myfunc);». Теперь в тесте вы можете получить myFunc из изолированной области и следить за ним.
Яир Тавор
22
У меня не работает. element.isolateScope()возвращается undefined. И element.scope()возвращает область видимости, которая не содержит всего того, что я поместил в свою область видимости.
mcv
4
@mcv Я обнаружил, что мне нужно сделатьelement.children().isolateScope()
Уилл Килинг
11

Ты можешь сделать var isolateScope = myDirectiveElement.scope() чтобы изолировать прицел.

На самом деле вам не нужно проверять, что вызывается $ watch ... это больше тестирование angularjs, чем тестирование вашего приложения. Но я думаю, это просто пример вопроса.

Эндрю Джослин
источник
2
Я не уверен, что согласен с тем, что это "тестирование angular". Я не проверяю, что $ watch работает, а просто то, что директива является свойством "wired-up" для angular.
daniellmb
1
Также daniellmb, способ проверить это - разоблачить вашу greetфункцию и шпионить за ней, и проверить, вызывается ли она, а не $ watch.
Эндрю Джослин
Да, это надуманный пример, но мне было интересно, есть ли чистый способ протестировать изолированную область видимости. Нарушение инкапсуляции и размещение методов в области видимости не сработает в этом случае, поскольку нет никакого крючка для добавления шпиона до его вызова.
daniellmb 08
@AndyJoslin, Из любопытства зачем isolateScopeвообще создавать переменную? См. Комментарий Энга к этому видео о яйцеголовых ( egghead.io/lessons/angularjs-unit-testing-directive-scope ): Начиная с Angular 1.2, для получения изолированной области необходимо использовать element.isolateScope()вместо element.scope() code.angularjs.org/1.2. 0 / docs / api / angular.element
Danger14
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);

и протестируйте последний, как и любой другой контроллер - напрямую передавая объект области (см. раздел Контроллеры здесь например, ).

если вам нужно запустить $ 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
  });
});
Никита
источник
0

Я не уверен, что это возможно с изолированным прицелом (хотя я надеюсь, что кто-то докажет, что я ошибаюсь). Изолированная область видимости, которая создается в директиве, в общем, изолирована, поэтому метод $ watch в директиве отличается от области, за которой вы наблюдаете в модульном тесте. Если вы измените scope: {} на scope: true, область действия директивы будет унаследована прототипом, и ваши тесты должны пройти.

Я думаю, это не самое идеальное решение, потому что иногда (в большинстве случаев) изолировать область видимости - это хорошо.

ЮникодСнеговик
источник