Cómo realizar una prueba unitaria de la directiva de alcance aislado en AngularJS

81

¿Cuál es una buena manera de realizar pruebas unitarias de alcance aislado en AngularJS?

JSFiddle mostrando prueba unitaria

Fragmento de directiva

    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);
        });
    }

Quiero asegurarme de que la directiva esté escuchando los cambios; esto no funciona con un alcance aislado:

    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));
    });

ACTUALIZACIÓN: Lo hice funcionar comprobando si los observadores esperados se agregaron al alcance del niño, pero es muy frágil y probablemente usa los accesores de una manera indocumentada (también conocida como sujeto a cambios sin previo aviso).

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

ACTUALIZACIÓN 2: ¡ Como mencioné, esto es frágil! La idea todavía funciona, pero en las versiones más recientes de AngularJS, el acceso ha cambiado de scope()a isolateScope():

//this is STILL super brittle, is there a better way!?
elm = compile(validHTML)(scope);                       
expect(elm.isolateScope().$$watchers[0].exp).toBe('name');
daniellmb
fuente
¿Encontraste una manera de configurar el espionaje?
tusharmath
@Tushar no realmente, ya que antes hay una manera de hacerlo funcionar, pero está sujeto a cambios sin previo aviso, así que úselo bajo su propio riesgo.
daniellmb

Respuestas:

102

Consulte los documentos de la API de elementos angulares . Si usa element.scope () , obtiene el alcance del elemento que definió en la propiedad de alcance de su directiva. Si usa element.isolateScope () obtiene todo el alcance aislado. Por ejemplo, si su directiva se parece a esto:

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

Luego, llamar a element.scope () en su prueba devolverá

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

Pero si llama a element.isolateScope () obtendrá

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

Esto es cierto a partir del angular 1.2.2 o 1.2.3, no estoy seguro exactamente. En versiones anteriores solo tenías element.scope ().

Yair Tavor
fuente
1
v1.2.3 feat (jqLite): exponer isolateScope () getter similar a scope () github.com/angular/angular.js/commit/…
daniellmb
1
pero ¿dónde espías el método $ watch?
tusharmath
1
podría exponer la función que se ejecuta en $ watch y luego espiarla. En la directiva, establezca "scope.myfunc = function () ...", luego en $ watch do "$ scope. $ Watch ('myName', scope.myfunc);". Ahora, en la prueba, puede obtener myFunc del alcance aislado y espiarlo.
Yair Tavor
22
No me funciona. element.isolateScope()devuelve undefined. Y element.scope()devuelve un alcance que no contiene todas las cosas que puse en mi alcance.
mcv
4
@mcv Descubrí que necesitaba hacerelement.children().isolateScope()
Will Keeling
11

Puede hacer var isolateScope = myDirectiveElement.scope()para obtener el alcance aislado.

Sin embargo, realmente no necesitas probar que se llamó a $ watch ... eso es más probar angularjs que probar tu aplicación. Pero supongo que es solo un ejemplo de la pregunta.

Andrew Joslin
fuente
2
No estoy seguro de estar de acuerdo en que sea "prueba angular". No estoy probando que $ watch funcione, sino simplemente que la directiva es propiedad "conectada" a angular.
daniellmb
1
También daniellmb, la forma de probar esto sería exponer su greetfunción y espiar eso, y verificar si se llama, no el $ watch.
Andrew Joslin
Bien, este es un ejemplo artificial, pero me interesó si existe una forma limpia de probar el alcance aislado. Romper la encapsulación y poner métodos en el alcance no funcionaría en este caso, ya que no hay ningún gancho para agregar el espía antes de que se llame.
daniellmb
@AndyJoslin, Por curiosidad, ¿por qué crear una isolateScopevariable? Vea el comentario de Ang sobre este video de cabeza de huevo ( egghead.io/lessons/angularjs-unit-testing-directive-scope ): A partir de Angular 1.2, para recuperar el alcance aislado, se necesita usar en element.isolateScope()lugar de element.scope() code.angularjs.org/1.2. 0 / docs / api / angular.element
Danger
1

mover la lógica a un controlador separado, es decir:

//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);

y pruebe este último como lo haría con cualquier controlador, pasando el objeto de alcance directamente (consulte la sección Controladores aquí para ver un ejemplo).

si necesita activar $ watch en su prueba, haga lo siguiente:

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
  });
});
Nikita
fuente
0

No estoy seguro de que sea posible con un alcance aislado (aunque espero que alguien me demuestre que estoy equivocado). El alcance aislado que se crea en la directiva está, bueno, aislado, por lo que el método $ watch en la directiva es diferente del alcance que está espiando en la prueba unitaria. Si cambia el alcance: {} a alcance: verdadero, el alcance de la directiva heredará prototípicamente y sus pruebas deberían pasar.

Supongo que esta no es la solución más ideal, porque a veces (muchas veces), aislar el alcance es algo bueno.

UnicodeMuñeco De Nieve
fuente