¿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');
Respuestas:
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 ().
fuente
element.isolateScope()
devuelveundefined
. Yelement.scope()
devuelve un alcance que no contiene todas las cosas que puse en mi alcance.element.children().isolateScope()
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.
fuente
greet
función y espiar eso, y verificar si se llama, no el $ watch.isolateScope
variable? 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 enelement.isolateScope()
lugar deelement.scope()
code.angularjs.org/1.2. 0 / docs / api / angular.elementmover 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 }); });
fuente
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.
fuente