Archives de Catégorie: Tips

5 astuces sur la façon d’écrire les directives AngularJS et leurs tests

S’il y a bien un sujet compliqué en Angular, c’est l’écriture de directives. J’espère que les chapitres de notre livre aident à passer un cap sur ce problème, mais il manque sur les internets un article un peu complet sur la façon de tester celles-ci.

Angular est très bien pensé pour les tests, avec un système de mock, d’injection de dépendance, de simulation des requêtes HTTP, bref la totale. Mais les tests de directive restent souvent le parent pauvre de tout ça.

Une directive un peu complète va contenir un template, un scope à elle avec différentes valeurs initialisées, et un ensemble de méthodes de comportement. Essayons de prendre une exemple pratique et pas trop compliqué :

    angular.module('myProject.directives').directive('gravatar', function() {
      return {
        restrict: 'E',
        replace: true,
        scope: {
          user: '=',
          size: '@'
        },
        template: '<img class="gravatar" ng-src="http://www.gravatar.com/avatar/{{ user.gravatar }}?s={{ sizePx }}&d=identicon"/>',
        link: function(scope) {
          if (scope.size === 'lg') {
            scope.sizePx = '40';
          } else {
            scope.sizePx = '20';
          }
        }
      };
    });

Cette directive permet d’afficher le gravatar d’un utilisateur (passé en paramètre user), avec 2 tailles possibles : 20px par défaut et 40px si le paramètre size est précisé avec la valeur lg. Cette logique de composant est assez agréable à manipuler, puisque pour l’utiliser, il suffit de mettre dans un template :

    <gravatar user="user" size="lg"></gravatar>

Tester une directive ressemble à un test classique, avec quelques instructions en plus qui ressemblent à des incantations de magie noire quand on débute, et que l’on copie/colle religieusement en espérant que personne ne nous pose de questions sur leur signification.

    beforeEach(inject(function($rootScope, $compile) {
      scope = $rootScope;
      scope.user = {
          gravatar: '12345',
          name: 'Cédric'
      };

      gravatar = $compile('<gravatar user="user" size="lg"></gravatar>')(scope);

      scope.$digest();
    }));

1. C’est quoi ce bordel ?!

On commence par créer une chaîne de caractères avec le HTML que l’on veut interpréter. Celui-ci doit, bien sûr, contenir la directive que vous voulez tester :

    '<gravatar user="user" size="lg"></gravatar>'

Ensuite l’élément est compilé : c’est peut-être la première fois que vous voyez le service $compile. Celui-ci est un service fourni par Angular, utilisé par le framework lui-même, mais rarement dans notre code. A l’exception des tests donc.
Pour le compiler, on lui passe un scope, qui correspond aux variables auxquelles la directive aura accès. La nôtre a, par exemple, besoin d’un utilisateur : on crée donc un scope avec une variable `user` qui contient l’id gravatar qui va bien.

Le $digest() à la fin permet de déclencher les watchers, c’est à dire résoudre toutes les expressions contenues dans notre directive : user.gravatar et sizePx.

Une fois compilée, on récupère un élément Angular, comme lorsque l’on utilise la méthode angular.element qui wrappe un élément de DOM ou du HTML sous forme de chaîne de caractères pour en faire un élément jQuery.

Et voilà, le setup est fait. Maintenant, nous allons pouvoir passer au test proprement dit.

Ce que vous ne savez probablement pas, c’est qu’un élément Angular offre de petits bonus. Ainsi, nous pouvons accéder au scope de la directive, qu’il soit isolé ou non. Dans notre cas, la directive gravatar utilise un scope isolé, donc notre test ressemblerait à quelque chose comme ça :

    it('should have the correct size on scope', function() {
        expect(gravatar.isolateScope().sizePx).toBe('40');
    });

Si le scope n’était pas isolé, on utiliserait `scope()` :

    it('should have the correct size on scope', function() {
        expect(gravatar.scope().sizePx).toBe('40');
    });

On peut aussi s’assurer que le HTML produit par la directive est conforme à ce que l’on attend. Vous pouvez utiliser la méthode html() qui renvoie le HTML de l’élément sous forme de chaîne de caractères, mais cela donne des tests un peu pénibles à maintenir. On peut faire quelque chose d’un peu plus sympa, pour tester la validité de l’élément, des classes ou attributs avec :

    it('should create a gravatar image with large size', function() {
        expect(gravatar[0].tagName).toBe('IMG');
        expect(gravatar.hasClass('gravatar')).toBe(true);
        expect(gravatar.attr('src')).toBe('http://www.gravatar.com/avatar/12345?s=40&d=identicon');
    });

Il est pas beau ce test ? Mais on peut encore mieux faire…

2. La logique dans un controller

La logique d’une directive peut être un pénible à tester. Le plus simple est de l’externaliser dans un controller dédié, que l’on peut tester comme un controller classique :

    angular.module('myProject.directives').directive('gravatar', function() {
      return {
        restrict: 'E',
        replace: true,
        scope: {
          user: '=',
          size: '@'
        },
        template: '<img class="gravatar" ng-src="http://www.gravatar.com/avatar/{{ user.gravatar }}?s={{ sizePx }}&d=identicon"/>',
        controller: 'GravatarDirectiveController'
      };
    });

C’est d’autant plus utile si votre controller grossit et devient plus complexe.

3. Externaliser le template

De la même façon, si le template grossit trop, n’hésitez pas à l’extraire dans un fichier à part.

    angular.module('myProject.directives').directive('gravatar', function() {
      return {
        restrict: 'E',
        replace: true,
        scope: {
          user: '=',
          size: '@'
        },
        templateUrl: 'gravatar.html',
        controller: 'GravatarDirectiveController'
      };
    });

Cela introduit cependant une petite subtilité pour les tests. Si vous relancez celui que vous aviez avant d’externaliser le template, vous allez avoir l’erreur suivante :

    Error: Unexpected request: GET gravatar.html
    No more request expected

Et oui, si on externalise le template, AngularJS va faire une requête pour le récupérer auprès du serveur. D’où une requête GET inattendue…
Mais on peut charger le template dans le test pour éviter ce problème. Il suffit pour cela d’utiliser karma-ng-html2js. Le principe est de charger les templates dans un module à part et d’inclure ce module dans notre test.

Il suffit alors de charger le template dans le test :

    beforeEach(module('gravatar.html'));

Et le tour est joué !

4. Récursivité

Si vous faites des directives un peu avancées, un jour ou l’autre, vous allez tomber sur une directive qui s’appelle elle-même. Bizarrement, ce n’est pas supporté par défaut par AngularJS. Vous pouvez cependant ajouter un module, RecursionHelper, qui offre un service permettant de compiler manuellement des directives récursives :

    angular.module('myProject.directives')
    .directive('container', function(RecursionHelper) {
      return {
        restrict: 'E',
        templateUrl: 'partials/container.html',
        controller: 'ContainerDirectiveCtrl',
        compile: function(element) {
          return RecursionHelper.compile(element, function() {
          });
        }
      };
    });

5. Apprendre des meilleurs

Le meilleur moyen de progresser en écriture de directives est de vous inspirer des projets open-source. Le projet AngularUI contient un grand nombre de directives, notamment les directives de UIBootstrap qui peuvent vous inspirer. L’un des principaux contributeurs au projet, Pawel, a fait un talk avec quelques idées complémentaires à cet article.

Et si vous voulez mettre tout ça en pratique, notre prochaine formation a lieu à Paris les 9-11 Février, et la suivante à Lyon les 9-11 Mars !

Git config – les options indispensables

Si vous utilisez Git, vous connaissez probablement la commande ‘git config’ qui permet de paramétrer Git. Vous savez peut être qu’il existe 3 niveaux possibles de stockage de ces paramètres :
– system (tous les utilisateurs)
– global (pour votre utilisateur)
– local (pour votre projet courant)

Si un paramètre est défini localement et globalement avec des valeurs différentes, ce sera la valeur locale qui sera utilisée (le niveau le plus bas surcharge les niveaux supérieurs).

Il est possible d’afficher sa configuration avec :

    git config --list

On peut ajouter une valeur très simplement, par exemple mon user.name (utilisé pour l’auteur des commits) :

    git config --global user.name cexbrayat

Et l’on peut supprimer une valeur avec –unset :

    git config --unset user.name

Savez vous que l’on peut également définir des alias de commande? Par exemple, j’utilise souvent l’alias ‘lg’ pour une version de ‘log’ améliorée :

    git config alias.lg “log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)%Creset' --abbrev-commit”

Mais revenons à nos paramétrages. La doc de Git est très riche et il n’est pas simple de savoir quelles options peuvent être utiles. Le fichier ‘.gitconfig’ dans votre ‘home’ contient tous vos paramétrages (lorsque vous les avez ajouté en global). Ce fichier regroupe les clés par section, par exemple user.name et user.email sont regroupées dans une section [user]. Vous allez voir, rien de compliqué!

Voici les paramètres que j’aime utiliser :

    [user]
        email = cedric@ninja-squad.com
        name = cexbrayat

Ces deux paramètres sont nécessaires pour faire un commit et apparaîtront dans le log de vos collègues, à compléter dès l’installation donc!

    [core]
        editor = vim
        pager = less
        excludesfile = ~/.gitignore_global

La section core contient beaucoup d’options possibles. Parmi elles, ‘editor’ vous permet de choisir quel éditeur de texte sera utilisé (j’aime bien vim, si si je vous assure, mais vous pouvez très bien mettre SublimeText par exemple, avec ‘subl -w’), idem pour le pager utilisé par Git (pour afficher le log par exemple). Il faut savoir que Git pagine dès que l’affichage ne rentre pas dans votre écran : certains détestent ça et préfèrent utiliser ‘cat’ plutôt.

L’option `excludesfile` permet d’ignorer de façon globale certains fichiers en les précisant dans un `.gitignore` global. Le mien est inspiré de celui de Github.

    [alias]
        lg = log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)%Creset' --abbrev-commit
        wdiff = diff --word-diff

Cette section contient tous les alias que vous ajouterez. Comme j’utilise oh-my-zsh et son plugin Git, j’ai déjà beaucoup d’alias disponibles (par exemple ‘gc’ pour ‘git commit’). On retrouve donc ‘lg’ pour un meilleur log et ‘wdiff’ pour un diff en mode ‘mot’ et pas ‘ligne’. Exemple avec ‘git diff’ :

    -:author:    Ninja Squad
    +:author:    Ninja Squad Team

Un seul mot ajouté, mais Git considère que la ligne entière est changée. Alors qu’avec ‘git wdiff’ :

    :author:    Ninja Squad{+ Team+}

Il reconnaît bien le seul ajout de mot sur la ligne!

    [color]
        ui = auto

Git est tout de suite plus convivial en ligne de commande avec un peu de couleur. Il est quasiment possible de définir pour chaque commande quelle couleur vous voulez, mais le plus simple est d’utiliser la clé ‘color.ui’ qui donne une valeur par défaut à toutes les commandes. A vous les branches en couleur, le log en couleur, le diff en couleur…

    [credential]
        helper = osxkeychain

Si vous utilisez un repo distant protégé par mot de passe (à tout hasard Github), il est possible de stocker votre mot de passe pour ne pas avoir à le saisir à chaque fois. Sur MacOS, le keychain se charge de le stocker pour vous.

    [clean]
        requireForce = false

Git possède une commande ‘clean’ pour supprimer les fichiers non suivis. Mettre requireForce à false permet d’éviter d’avoir à ajouter le flag ‘-f’.

    [diff]
        mnemonicprefix = true

Par défaut lorsque vous faites un ‘diff’, Git vous affiche par exemple :

    --- a/Git/git.asc
    +++ b/Git/git.asc

Ces préfixes a/ et b/ ne sont pas très parlants n’est ce pas? En passant ‘diff.mnemonicprefix’ à true, Git va afficher des préfixes plus logiques, par exemple ‘i’ pour index et ‘w’ pour le work tree (votre dossier de travail). Exemple :

    --- i/Git/git.asc
    +++ w/Git/git.asc

Pratique.

    [help]
        autocorrect = -1

L’une de mes fonctionnalités préférées! Si comme moi vous avez tendance à entrer ‘git rzset’ comme commande, vous avez déjà vu Git vous dire :

Did you mean this?
    reset

Mais ne rien faire pour autant! Et bien en activant l’autocorrection, git va remplacer votre commande par ‘git reset’ automatiquement. Il est possible de laisser un délai en donnant un entier positif comme valeur, ici, avec -1, la correction sera immédiate.

    [rerere]
        enabled = true

Si vous avez lu mon dernier article sur Git, vous savez déjà tout le bien que peut vous apporter ‘rerere’!

    [push]
        default = upstream

Actuellement (Git < 2.0), Git pousse toutes les branches modifiées qui existent sur le repo distant lors d’une commande ‘git push’ (valeur ‘matching’). Je trouve toujours ça un peu dangereux, car on a parfois oublié que l’on a des modifications sur une autre branche que celle en cours, et que l’on ne voudrait pas forcément partager immédiatement. A partir de Git 2.0, la nouvelle valeur sera ‘simple’ : seule la branche courante sera poussée si une branche du même nom existe. La valeur que je préfère est ‘upstream’, qui comme ‘simple’, permet de pousser seulement la branche locale, mais ce même si la branche distante ne possède pas le même nom.

    [rebase]
        autosquash = true
        autostash = true

Lorsqu’on effectue un rebase, Git propose la liste des commits concernés avec un verbe d’action pour chacun (en l’occurence ‘pick’). Ce verbe d’action peut être modifié pour devenir ‘reword’, ‘edit’, ‘squash’ ou ‘fixup’. Les deux derniers compressent deux commits en un seul. Parfois je sais dès que je commite qu’il devra fusionner avec le précédent. En préfixant le message de commit par ‘fixup!’, lors du rebase qui viendra, le commit sera automatiquement précédé par le verbe ‘fixup’ plutôt que ‘pick’!

L’option ‘rebase.autostash’ est toute récente (release 1.8.4.2 de Git) et permet d’automatiser une opération un peu pénible. Lorsque l’on lance un rebase, le répertoire de travail doit être sans modification en cours, sinon le rebase ne se lance pas. Il faut alors commiter ou stasher les modifications avant de recommencer le rebase. Avec cette option ‘rebase.autostash’, le rebase mettra dans automatiquement les modifications courantes dans le stash avant de faire le rebase, puis ré-appliquera les modifications ensuite.

    [pull]
        rebase = true

Par défaut, ‘pull’ effectue un ‘fetch’ suivi d’un ‘merge’. Avec l’option ‘rebase’, ‘pull’ effectuera un ‘fetch’ suivi d’un ‘rebase’.

Allez, pour votre culture, un petit tour des sections moins utiles mais parfois intéressantes.

    [advice]

Il est possible de désactiver tous les conseils que vous affiche Git en cas de problème. C’est un peu comme activer le mode difficile de Git, c’est vous qui voyez…

    [commit]

Il est possible de customiser le template de message de commit avec l’option ‘commmit.template’. Cela peut être pratique si vous avez des pratiques partagées par toute l’équipe (comme celles de l’équipe AngularJS).

Vous pouvez trouver mon ‘.gitconfig’ complet sur mon repo Github.

Et vous, quelles sont vos options préférées?

Git rerere – ma commande préférée

J’adore faire des rebases. Vraiment. C’est d’ailleurs une part très importante du workflow que nous vous conseillons. L’une des choses qui énerve parfois ceux qui font des rebases vient de la résolution de conflit répétitives qui peut survenir.

Pour rappel, un rebase va prendre tous les commits sur votre branche courante et les rejouer un à un sur une autre. Si vous avez une branche ‘topic1’ avec 5 commits et que vous effectuer un rebase sur la branche ‘master’ alors Git rejoue les 5 commits les uns après les autres sur la branche ‘master’.

Parfois, ces commits ont un conflit et le rebase s’arrête, le temps de vous laisser corriger. Mais il arrive que dès que Git passe au commit suivant, vous ayez à nouveau le conflit! Ce qui est très frustrant et pousse parfois à l’abandon du rebase. Cette situation arrive aussi lorsque l’on doit merger une modification en série sur des branches plus récentes. Par exemple, un bugfix sur la version 1.1, qui doit être mergé sur les branches 1.2 et 2.0. Si un conflit de merge apparaît lors du merge sur 1.2, il est frustrant de l’avoir à nouveau sur la branche 2.0 une fois celui-ci résolu.

Mais, vous vous en doutez, je vous parle de tout ça car Git propose une option à activer une seule fois pour être débarrassé de ces soucis.

Cette commande a le nom le plus improbable de toutes les commandes. rerere signifie reuse recorded resolution : cette commande permet à Git de se rappeler de quelle façon un conflit a été résolu et le résoudre automatiquement de la même façon la prochaine que ce conflit se présente.

Pour activer rerere, la seule chose à faire est de l’indiquer en configuration :

$ git config --global rerere.enabled true

Une fois activé, Git va se souvenir de la façon dont vous résolvez les conflits, sans votre intervention. Par exemple, avec un fichier nommé bonjour contenant sur master :

hello ninjas

Une branche french est créée pour la version française :

bonjour ninjas

Alors que sur master, une modification est appliquée

hello ninjas!

Si la branche french est mergée, alors un conflit survient :

Auto-merging bonjour
CONFLICT (content): Merge conflict in bonjour
Recorded preimage for 'bonjour'
Automatic merge failed; fix conflicts and then commit the result.

Si l’on édite le fichier, on a bien un conflit :

<<<<<<< HEAD
hello ninjas!
=======
bonjour ninjas
>>>>>>> french

Vous pouvez voir les fichiers en conflit surveillés par rerere :

$ git rerere status
bonjour

Vous corrigez le conflit, pour conserver :

bonjour ninjas!

Vous pouvez voir ce que rerere retient de votre résolution avec :

$ git rerere diff
--- a/bonjour
+++ b/bonjour
@@ -1,5 +1 @@
-<<<<<<<
-bonjour ninjas
-=======
-hello ninjas!
->>>>>>>
+bonjour ninjas!

Une fois terminée la résolution du conflit (add et commit), vous pouvez voir la présence d’un nouveau répertoire dans le dossier .git, nommé rr-cache, qui contient maintenant un dossier correspondant à notre résolution dans lequel un fichier conserve le conflit (preimage) et la résolution (postimage).

Maintenant, vous vous rendez compte que vous vous préféreriez un rebase plutôt qu’un merge. Pas de problème, on reset le dernier merge :

$ git reset --hard HEAD~1

On se place sur la branche french et on rebase.

$ git checkout french
$ git rebase master
...
Falling back to patching base and 3-way merge...
Auto-merging bonjour
CONFLICT (content): Merge conflict in bonjour
Resolved 'bonjour' using previous resolution.
Failed to merge in the changes.
Patch failed at 0001 bonjour ninjas!

Nous avons le même conflit que précédemment, mais cette fois on peut voir « Resolved bonjour using previous resolution. ». Et si nous ouvrons le fichier bonjour, le conflit a été résolu automatiquement!

Par défaut, rerere n’ajoute pas le fichier à l’index, vous laissant le soin de vérifier la résolution et de continuer le rebase. Il est possible avec l’option ‘rerere.autoupdate’ de faire cet ajout automatiquement à l’index (je préfère personnellement laisser cette option à ‘false’ et vérifier moi-même)!

A noter qu’il serait possible de remettre le fichier avec son conflit (si la résolution automatique ne vous convenait pas) :

$ git checkout --conflict=merge bonjour

Le fichier est alors à nouveau en conflit :

<<<<<<< HEAD
hello ninjas!
=======
bonjour ninjas
>>>>>>> french

Vous pouvez re-résoudre automatiquement le conflit avec :

$ git rerere

Magique!

Play tips – Test with an embedded mongodb

Si il y a deux outils que j’affectionne en ce moment, c’est bien Play! (v1.2.5, la v2 ne m’a pas encore convaincu) et MongoDB. Pour ceux qui ne connaissent pas Mongo, c’est une base de données NoSQL orientée document qui écrase un peu la concurrence actuellement. Pour synthétiser les points forts de celle-ci en quelques lignes (car ce n’est pas le sujet de ce billet), on vous dira que :

– MongoDb n’a pas de schéma, vous n’avez qu’à envoyer un objet en JSON, et il stocké, point barre.
– MongoDB va stocker votre objet comme un document en BSON (un encodage binaire du JSON)
– MongoDB vous donne une API javascript pour faire des requêtes, mais des drivers sont existants dans la plupart des languages.
– MongoDB est très performant. Et qu’en plus si une instance ne suffit pas en prod, vous pouvez en mettre plusieurs, et le sharding (la répartition de vos données entre les différentes instances) se fait tout seul.
– Mongo gère la réplication entre instances, mais également la consistence de votre base (ce qui n’est pas le cas de toutes les bases NoSQL) à travers un fichier de log des opérations entre autres.
Et globalement tout ceci est assez vrai. Mongo n’est surement pas la base de données ultime mais elle rend de très bons services si l’on a des choses un peu volumineuses à stocker.

Si vous avez testé Play!, vous avez surement déjà remarqué le système de persistence assez bien foutu. On annote une entité, on lui fait étendre une classe Model et ça roule. Et bien, pour MongoDB c’est à peu près la même chose.

Vous devez commencer par ajouter Morphia dans les dépendances du projet. Morphia (hébergé sous google code, si je vous jure, ça existe encore) est un petit ORM Java/MongoDB qui marche pas mal, et justement un module Play existe. on édite donc le fichier dependencies.yml pour ajouter Morphia.

 - play -> morphia 1.2.9

Un petit coup de “play deps” en ligne de commande pour récupérer le module et nous sommes prêts. Dans votre projet, prenez (ou créez) une entité, par exemple App

Si on ajoute un simple test unitaire

et qu’on le lance, le test échoue lamentablement à se connecter à votre MongoDB. Normal! Et le but de ce billet et de vous montrer comment lancer ces tests d’intégration sans installer MongoDB sur une machine. Pour cela une petite lib très pratique existe : embedmongo. Vous pouvez l’ajouter dans votre fichier de dépendances :

- de.flapdoodle.embedmongo -> de.flapdoodle.embedmongo 1.16

Nous devons compléter notre test unitaire pour démarrer une instance Mongo avant le test. La plupart des articles illustrant l’utilisation de cette librairie montre des exemples de tests unitaires où l’instance est démarrée dans une méthode annotée @BeforeClass (qui, comme vous le savez sans doute, sera executée avant les tests). Mais Play! fonctionne un peu différement puisqu’un test unitaire Play! (qui étend la classe UnitTest) démarre en fait l’application avant de lancer la classe de tests et notamment les plugins utilisés comme Morphia. Si nous utilisons une méthode annotée @BeforeClass , le test va échouer car Morphia ne parviendra pas à se connecter à l’instance embarquée qui ne sera pas encore démarrée. Un moyen simple de contournement est de créer notre propre classe de base pour les tests unitaires qui utilisera notre Test Runner plutôt que celui par défaut de Play! En fait, notre Runner va tout simplement étendre le Runner de base mais démarrera l’instance Mongo avant de démarrer Play! Get it ?

Voila donc notre classe de base de test (qui remplace UnitTest et utilise donc notre propre runner de tests)

et notre Runner, qui démarre l’instance avant d’appeler le runner de Play!

Notre test devient

Tadam! Cette fois le test fonctionne!

Enjoy!