web-dev-qa-db-fra.com

Comment écrire des contrôleurs testables avec des méthodes privées dans AngularJs?

Très bien, donc je suis tombé sur un problème depuis longtemps et j'aimerais entendre une opinion du reste de la communauté.

Tout d'abord, regardons un contrôleur abstrait.

function Ctrl($scope, anyService) {

   $scope.field = "field";
   $scope.whenClicked = function() {
      util();
   };

   function util() {
      anyService.doSmth();
   }

}

De toute évidence, nous avons ici:

  • échafaudage régulier pour contrôleur avec $scope et certains services injectés
  • un domaine et une fonction attachés à la portée
  • méthode privée util()

Maintenant, je voudrais couvrir cette classe dans des tests unitaires (Jasmine). Cependant, le problème est que je veux vérifier que lorsque je clique sur (appeler whenClicked()) un élément que la méthode util() sera appelée. Je ne sais pas comment faire, car dans les tests Jasmine, je reçois toujours des erreurs indiquant que la maquette de util() n'a pas été définie ou n'a pas été appelée.

Remarque: je n'essaie pas de corriger cet exemple particulier, je pose des questions sur le test d'un tel modèle de code en général. Donc, ne me dites pas "quelle est l'erreur exacte". pas comment résoudre ce problème.

J'ai essayé un certain nombre de façons de contourner cela:

  • évidemment, je ne peux pas utiliser $scope dans mes tests unitaires car je n'ai pas cette fonction attachée à cet objet (cela se termine généralement par le message Expected spy but got undefined ou similaire)
  • J'ai essayé d'attacher ces fonctions à l'objet contrôleur via Ctrl.util = util;, Puis de vérifier des simulations comme Ctrl.util = jasmine.createSpy() mais dans ce cas, Ctrl.util N'est pas appelé, donc les tests échouent
  • J'ai essayé de changer util() pour être attaché à this objet et en me moquant de nouveau Ctrl.util, Sans succès

Eh bien, je ne trouve pas mon chemin, j'attendrais de l'aide des ninjas JS, un violon fonctionnel serait parfait.

56
ŁukaszBachman

Le nommer sur la portée est la pollution. Ce que vous voulez faire est d'extraire cette logique dans une fonction distincte qui est ensuite injectée dans votre contrôleur. c'est à dire.

function Ctrl($scope, util) {

   $scope.field = "field";
   $scope.whenClicked = function() {
      util();
   };
}

angular.module("foo", [])
       .service("anyService", function(...){...})
       .factory("util", function(anyService) {
              return function() {
                     anyService.doSmth();
              };
       });

Vous pouvez maintenant tester unitairement avec des simulations votre Ctrl ainsi que "util".

32
MikeMac

La fonction de contrôleur que vous avez fournie sera utilisée par Angular en tant que constructeur; à un moment donné, elle sera appelée avec new pour créer l'instance de contrôleur réelle. Si vous avez vraiment besoin d'avoir les fonctions de votre objet contrôleur qui ne sont pas exposées à la portée $ mais qui sont disponibles pour l'espionnage/le tronçonnage/la moquerie, vous pouvez les attacher à this.

function Ctrl($scope, anyService) {

  $scope.field = "field";
  $scope.whenClicked = function() {
    util();
  };

  this.util = function() {
    anyService.doSmth();
  }
}

Lorsque vous appelez maintenant var ctrl = new Ctrl(...) ou utilisez le service Angular $controller Pour récupérer l'instance Ctrl, l'objet renvoyé contiendra le util fonction.

Vous pouvez voir cette approche ici: http://jsfiddle.net/yianisn/8P9Mv/

42
yianis

Je vais jouer avec une approche différente. Vous ne devriez pas tester des méthodes privées. C'est pourquoi ils sont privés - c'est un détail d'implémentation qui n'est pas pertinent pour l'utilisation.

Par exemple, que se passe-t-il si vous vous rendez compte que cet utilitaire a été utilisé à plusieurs endroits, mais maintenant, sur la base d'un autre refactoring de code, il n'est appelé qu'à cet endroit. Pourquoi avoir un appel de fonction supplémentaire? Incluez simplement anyService.doSmith() à l'intérieur de vous $scope.whenClicked() Avec les suggestions ci-dessus, en supposant que vous testez que util() est appelé, vos tests s'arrêteront même si vous n'avez pas changé le fonctionnalité du programme. L'une des principales valeurs des tests unitaires est de simplifier le refactoring sans casser les choses, donc si vous ne les avez pas cassées, le test ne devrait pas échouer.

Ce que vous devez faire est de vous assurer que lorsque $scope.whenClicked Est appelé, anyService.doSmth() est également appelé. Vous avez juste besoin:

spyOn(anyService,'doSmith')
scope.whenClicked();
expect(anyService.doSmith).toHaveBeenCalled();
7
Yehosef

J'ajoute une réponse contenant mon approche actuelle, dans l'espoir d'obtenir des commentaires et peut-être de susciter une discussion sur la question de savoir si c'est une bonne solution.

Nous attachons des fonctions privées à la fonction de contrôleur (les rendant ainsi publiques, ce qui permet de se moquer). Pour éviter d'avoir à répéter le nom du contrôleur tout le temps et rendre la syntaxe plus attrayante, nous créons un objet self qui contient une référence à la fonction du contrôleur. Cela devient donc:

function Ctrl($scope, anyService) {

   $scope.field = "field";
   $scope.whenClicked = function() {
      self.util();
   };

   var self = Ctrl; // For the sake of syntax simplicity only

   self.util = function() {
      anyService.doSmth();
   };

}

puis dans les tests unitaires, nous pouvons maintenant utiliser:

Ctrl.util = jasmine.createSpy("util()");
expect(Ctrl.util).toHaveBeenCalled();

Je n'aime toujours pas beaucoup ça, mais je pense que c'est la façon la plus simple de le faire. J'espère que quelqu'un trouvera une meilleure approche.

2
ŁukaszBachman