web-dev-qa-db-fra.com

bindToController dans les tests unitaires

J'utilise bindToController dans une directive pour que la portée isolée soit directement attachée au contrôleur, comme ceci:

app.directive('xx', function () {
  return {
    bindToController: true,
    controller: 'xxCtrl',
    scope: {
      label: '@',
    },
  };
});

Ensuite, dans le contrôleur, j'ai un défaut si le libellé n'est pas spécifié dans le code HTML:

app.controller('xxCtrl', function () {
  var ctrl = this;

  ctrl.label = ctrl.label || 'default value';
});

Comment puis-je instancier xxCtrl dans les tests unitaires Jasmine afin de pouvoir tester le ctrl.label?

describe('buttons.RemoveButtonCtrl', function () {
  var ctrl;

  beforeEach(inject(function ($controller) {
    // What do I do here to set ctrl.label BEFORE the controller runs?
    ctrl = $controller('xxCtrl');
  }));

  it('should have a label', function () {
    expect(ctrl.label).toBe('foo');
  });
});

Vérifiez this pour tester le problème

30
ernestoalejo

Dans Angular 1.3 (voir ci-dessous pour 1.4+)

En fouillant dans le code source AngularJS, j'ai trouvé un troisième argument non documenté du service $controller appelé later (voir $ controller source ).

Si true, $controller() renvoie une fonction avec une propriété instance sur laquelle vous pouvez définir des propriétés.
Lorsque vous êtes prêt à instancier le contrôleur, appelez la fonction pour instancier le contrôleur avec les propriétés disponibles dans le constructeur.

Votre exemple fonctionnerait comme ceci:

describe('buttons.RemoveButtonCtrl', function () {

  var ctrlFn, ctrl, $scope;

  beforeEach(inject(function ($rootScope, $controller) {
    scope = $rootScope.$new();

    ctrlFn = $controller('xxCtrl', {
      $scope: scope,
    }, true);
  }));

  it('should have a label', function () {
    ctrlFn.instance.label = 'foo'; // set the value

    // create controller instance
    ctrl = ctrlFn();

    // test
    expect(ctrl.label).toBe('foo');
  });

});

Voici un Plunker mis à jour (il fallait mettre à jour Angular pour le faire fonctionner, il est maintenant 1.3.0-rc.4): http://plnkr.co/edit/tnLIyzZHKqPO6Tekd804?p=preview

Notez qu'il n'est probablement pas recommandé de l'utiliser, pour citer le code source Angular:

Instancier ultérieurement le contrôleur: cette machine est utilisée pour créer une instance De l'objet avant d'appeler le constructeur du contrôleur Lui-même.

Cela permet d'ajouter des propriétés au contrôleur avant que le constructeur Ne soit appelé. Cela est principalement utilisé pour les liaisons isolate scope Dans $ compile.

Cette fonctionnalité n'est pas destinée à être utilisée par des applications et n'est donc pas Documentée publiquement.

Cependant, le manque de mécanisme pour tester les contrôleurs avec bindToController: true m'a obligé à l'utiliser quand même. Peut-être que les gars de typeAngular devraient envisager de rendre ce drapeau public.

Sous le capot, il utilise un constructeur temporaire, nous pourrions également l'écrire nous-mêmes, je suppose.
L’avantage de votre solution est que le constructeur n’est pas appelé deux fois, ce qui peut poser problème si les propriétés n’ont pas de valeur par défaut comme dans votre exemple.

Angular 1.4+ (Mise à jour du 2015-12-06):
L'équipe Angular a ajouté la prise en charge directe de cette fonctionnalité dans version 1.4.0 . (Voir # 9425 )
Vous pouvez simplement passer un objet à la fonction $controller:

describe('buttons.RemoveButtonCtrl', function () {

  var ctrl, $scope;

  beforeEach(inject(function ($rootScope, $controller) {
    scope = $rootScope.$new();

    ctrl = $controller('xxCtrl', {
      $scope: scope,
    }, {
      label: 'foo'
    });
  }));

  it('should have a label', function () {
    expect(ctrl.label).toBe('foo');
  });
});

Voir aussi ce blog post .

47
meyertee

Test d'unité BindToController à l'aide de ES6

Si vous utilisez ES6, vous pouvez importer directement le contrôleur et effectuer un test sans utiliser de mock angulaire.

Directive:

import xxCtrl from './xxCtrl';

class xxDirective {
  constructor() {
    this.bindToController = true;
    this.controller = xxCtrl;
    this.scope = {
      label: '@'
    }
  }
}

app.directive('xx',  new xxDirective());

Contrôleur:

class xxCtrl {
  constructor() {
    this.label = this.label || 'default value';
  }
}

export default xxCtrl;

Test du contrôleur:

import xxCtrl from '../xxCtrl';

describe('buttons.RemoveButtonCtrl', function () {

  let ctrl;

  beforeEach(() => {
    xxCtrl.prototype.label = 'foo';
    ctrl = new xxCtrl(stubScope);
  });

  it('should have a label', () => {
    expect(ctrl.label).toBe('foo');
  });

});

voir ceci pour plus d'informations: Test unitaire correct de applications JS angulaires avec modules ES6

3
EnugulaS

À mon avis, ce contrôleur n'est pas destiné à être testé de manière isolée, car il ne fonctionnera jamais de manière isolée:

app.controller('xxCtrl', function () {
  var ctrl = this;

  // where on earth ctrl.lable comes from???
  ctrl.newLabel = ctrl.label || 'default value';
});

Il est étroitement associé à la directive qui repose sur la réception des propriétés de son champ d’application. Ce n'est pas réutilisable. En regardant ce contrôleur, je dois me demander d'où vient cette variable. Ce n'est pas mieux qu'une fonction qui fuit en interne en utilisant une variable de l'extérieur de la portée:

function Leaky () {

    ... many lines of code here ...

    // if we are here we are too tired to notice the leakyVariable:
    importantData = process(leakyVariable);

    ... mode code here ...

    return unpredictableResult;
}

Maintenant, j'ai une fonction qui fuit dont le comportement est hautement imprévisible en fonction de la variable leakyVariable présente (ou non) dans la portée de la portée de cette fonction. 

Sans surprise, cette fonction est un cauchemar à tester. Ce qui est en fait une bonne chose, peut-être pour forcer le développeur à réécrire la fonction en quelque chose de plus modulaire et réutilisable. Ce qui n'est pas difficile vraiment:

function Modular (outsideVariable) {
    ... many lines of code here ...

    // no need to hit our heads against the wall to wonder where the variable comes from:
    importantData = process(outsideVariable);

    ... mode code here ...

    return predictableResult;   
}

Aucun problème de fuite et très facile à tester et à réutiliser. Ce qui me dit que le bon vieux $scope est un meilleur moyen:

app.controller('xxCtrl', function ($scope) {
  $scope.newLabel = $scope.label || 'default value';
});

Simple, court et facile à tester. Plus aucune définition d'objet directive volumineuse.

Le raisonnement d'origine de la syntaxe controllerAs était la portée qui fuyait du parent. Cependant, le champ d'application isolé de la directive résout déjà ce problème. Ainsi, je ne vois aucune raison d'utiliser la syntaxe volumineuse qui fuit.

1
Dmitri Zaitsev

J'ai trouvé un moyen qui n'est pas particulièrement élégant mais qui fonctionne au moins (s'il y a une meilleure option, laissez un commentaire).

Nous définissons la valeur qui "vient" de la directive, puis nous appelons à nouveau la fonction de contrôleur pour tester quoi que ce soit. J'ai fait un assistant "invokeController" pour être plus sec.

Par exemple:

describe('buttons.RemoveButtonCtrl', function () {

  var ctrl, $scope;
  beforeEach(inject(function ($rootScope, $controller) {
    scope = $rootScope.$new();
    ctrl = $controller('xxCtrl', {
      $scope: scope,
    });
  }));

  it('should have a label', function () {
    ctrl.label = 'foo'; // set the value

    // call the controller again with all the injected dependencies
    invokeController(ctrl, {
      $scope: scope,
    });

    // test whatever you want
    expect(ctrl.label).toBe('foo');
  });

});


beforeEach(inject(function ($injector) {
  window.invokeController = function (ctrl, locals) {
    locals = locals || {};
    $injector.invoke(ctrl.constructor, ctrl, locals);
  };
}));
0
ernestoalejo