AngularJS/tutorial

Cette page a été rédigée il y a fort fort longtemps, et n'a pas tellement été mise à jour.

 

Vous savez, moi je ne crois pas qu'il y ait de bonne ou de mauvaise page. Moi, si je devais résumer mon wiki aujourd'hui avec vous, je dirais que c'est d'abord des rencontres. Des gens qui m'ont tendu la main, peut-être à un moment où je ne pouvais pas, où j'étais seul chez moi. Et c'est assez curieux de se dire que les hasards, les rencontres forgent une destinée... Parce que quand on a le goût de la chose, quand on a le goût de la chose bien faite, le beau geste, parfois on ne trouve pas l'interlocuteur en face je dirais, le miroir qui vous aide à avancer. Alors ça n'est pas mon cas, comme je disais là, puisque moi au contraire, j'ai pu ; et je dis merci au wiki, je lui dis merci, je chante le wiki, je danse le wiki... je ne suis qu'amour ! Et finalement, quand des gens me disent « Mais comment fais-tu pour avoir cette humanité ? », je leur réponds très simplement que c'est ce goût de l'amour, ce goût donc qui m'a poussé aujourd'hui à entreprendre une construction logicielle... mais demain qui sait ? Peut-être simplement à me mettre au service de la communauté, à faire le don, le don de soi.

Les quelques notes de cet article indiquent comment procéder pour démarrer simplement avec le tutoriel Angular, sans Git.

Installation des outils

Pour commencer, il faut installer Node.js qui est nécessaire pour l’exécution des tests. Node.js est aussi utilisé comme serveur web pour l’application.

On ajoute ensuite les modules nécessaires pour les tests :

 $ npm -g install jasmine-node
 $ npm -g install testacular

Remarques :

  • les commandes sont exécutées en mode console

  • $ représente le prompt de la console

  • npm = Node.js Package Manager

  • l’option -g permet d’installer les modules dans un répertoire global, i.e. pour tous les utilisateurs (cette option est recommandée, sinon les outils seront installés uniquement pour votre projet)

  • Jasmine est un framework de test de code JavaScript écrit en Ruby

  • jasmine-node est la version Jasmine pour Node.js ⇒ pas la peine d’installer Ruby

  • Testacular est le moteur d’exécution des tests JavaScript

  • sous MacOS X, ces commandes sont exécutées avec sudo (probablement sous Linux aussi)

En ce qui concerne le serveur web qui nous permettra de visualiser les pages développées, le tutoriel d’Angular suggère d’installer le script web-server.js du projet corrigé angular-phonecat. Je préfère pour ma part installer le module http-server de Node.js., la manip étant plus simple :

 $ npm -g install http-server

Quelques commandes Node.js utiles :

  • npm -g ls pour lister les modules du répertoire global

  • npm -g rm nom_du_module pour supprimer un module

Corrigé de l’application angular-phonecat

Quand on progresse dans le tutorial, le code utilise des fichiers css, des bibliothèques JavaScript et des images. Il vaut mieux télécharger le projet angular-phonecat afin d’avoir les éléments sous la main le moment venu.

Structure initiale du projet

Avant de commencer le tutoriel, créer l’arborescence suivante :

  • projet

  • app

  • config

  • test

Étapes du tutoriel

Step 1 - Static Template

Créer le fichier index.html dans le répertoire app.

  • projet

  • app

  • index.html

  • config

  • test

Le répertoire projet représente l’espace de travail.

En mode console dans le répertoire projet, démarrer le serveur web avec le module http-server de Node.js sur le port 8000 :

 $ http-server . -p 8000

Remarque : le caractère "." représente le répertoire courant

La page index.html est accessible depuis l’url http://localhost:8000/app/index.html. Le serveur peut être arrêté avec un ctrl C.

Step 2 - Angular Templates

Fichiers et répertoires à copier / créer :

  • copier le fichier angular.js depuis projet corrigé dans le répertoire app/lib/angular

  • créer le fichier controllers.js dans le répertoire js

  • créer le répertoire test/unit

  • créer le fichier controllersSpec.js dans le répertoire test/unit

  • projet

  • app

  • index.html

  • js

  • controllers.js

  • lib

  • angular

  • angular.js

  • lib

  • config

  • testacular.conf.js

  • test

  • unit

  • controllersSpec.js

Créer le fichier de configuration pour les tests unitaires :

  • sous le répertoire projet, exécuter la commande testacular init

  • testing framework = jasmine

  • files to test = test/**/*Spec.js

  • autres options = valeurs proposées par défaut

  • déplacer le fichier de configuration testacular.conf.js dans le répertoire config

  • modifier le basePath du fichier testacular.conf.js : basePath = '../';

  • démarrer le serveur de test dans une seconde console, depuis le répertoire config avec la commande testacular start (le serveur peut être arrêté avec un ctrl C)

Step 3 - Filtering Repeaters

Dans cette étape, on apprend à utiliser les repeaters, les filters et à faire des tests end-to-end.

Fichiers et répertoires à copier / créer :

  • depuis le projet corrigé, copier img et css dans le répertoire app

  • créer le répertoire e2e dans le répertoire test

  • créer le fichier scenarios.js dans le répertoire e2e

  • copier le fichier runner.html dans le répertoire e2e depuis le projet corrigé

  • créer le répertoire lib/angular dans le répertoire test

  • copier le fichier angular-scenario.js dans le répertoire test/lib/angular depuis le projet corrigé :

  • projet

  • app

  • index.html

  • css

  • app.css

  • bootstrap-responsive.css

  • bootstrap-responsive.min.css

  • bootstrap.css

  • bootstrap.min.css

  • img

  • un ensemble d’images…​

  • js

  • controllers.js

  • lib

  • angular

  • angular.js

  • config

  • test

  • e2e

  • runner.html

  • scenarios.js

  • lib

  • angular

  • angular-scenario.js

  • unit

  • controllersSpec.js

La page d’exécution des tests est accessible depuis l’url http://localhost:8000/test/e2e/runner.html

Step 5 - XHRs and Dependency Injection

Dans cette étape des objets mock sont créés pour les tests end-to-end. Il faut donc ajouter la dépendance angular-mocks :

  • copier le fichier angular-mocks.js dans le répertoire test/lib/angular du projet

  • ajouter angular-mocks.js dans la liste des fichiers de testacular.conf.js (attention à l’odre de déclaration des fichiers) :

 files = [
   JASMINE,
   JASMINE_ADAPTER,
   'app/lib/angular/angular.js',
   'test/lib/angular/angular-mocks.js',
   'app/js//*.js',
   'test//*Spec.js'
 ];

Nous avons aussi besoin des fichiers json qui sont dans le répertoire phones :

  • projet

  • app

  • index.html

  • css

  • app.css

  • bootstrap-responsive.css

  • bootstrap-responsive.min.css

  • bootstrap.css

  • bootstrap.min.css

  • img

  • un ensemble d’images…​

  • js

  • controllers.js

  • lib

  • angular

  • angular.js

  • phones

  • config

  • test

  • e2e

  • runner.html

  • scenarios.js

  • lib

  • angular

  • angular-scenario.js

  • angular-mocks.js

  • unit

  • controllersSpec.js

Tests unitaires : commentaires sur la syntaxe

À la première lecture, il n’est pas évident de comprendre la syntaxe de l’injection du service $http. Tout d’abord, un rappel sur le mécanisme d’injection :

Angular se fonde sur le nom des arguments passés au contructeur du contrôleur : ces noms doivent correspondre aux noms de services déclarés auprès d’Angular pour que l’injection soit réalisée. Dans le cas du contrôleur PhoneListCtrl, les objets $scope et $http sont injectés : il s’agit de services Angular prédéfinis (cf $http et $scope).

 function PhoneListCtrl($scope, $http) {...}

Revenons maintenant au code du test unitaire.

1- Pourquoi le service $httpBackend est injecté dans le beforeEach et non pas le service $http ?

Tout simplement parce que le service $http délègue de façon sous-jacente les traitements au service $httpBackend et Angular fournit un objet de leurre pour $httpBackend, et non pas pour $http, pour des raisons d’organisation du code j’imagine. On modifie donc le comportement à l’intérieur du moteur, pas le moteur lui-même.

2- OK, je fais un objet de leurre pour $httpBackend, mais comment l’objet $http manipulé dans le contrôleur connait-il mon objet mock ?

En fait, le service $httpBackend est injecté dans le service $http dans tous les scénarii d’exécution : applicatif ou tests unitaires. Donc, encore une fois, en modifiant $httpBackend, on modifie indirectement $http.

3- Très bien, l’objet de leurre, c’est $httpBackend, alors pourquoi on injecte $httpBackend dans le beforeEach, et non pas $httpBackend lui-même ?

On peut effectivement injecter $httpBackend directement, et tout fonctionne parfaitement. Attention alors de bien renommer la variable $httpBackend du code de test, comme par exemple dans le code ci-dessous :

 var scope, ctrl, httpBackend;

 beforeEach(inject(function($httpBackend, $rootScope, $controller) {
   httpBackend = $httpBackend;
   httpBackend.expectGET('phones/phones.json').
         respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);

   scope = $rootScope.$new();
   ctrl = $controller(PhoneListCtrl, {$scope: scope});
 }));

 it('should create "phones" model with 2 phones fetched from xhr', function() {
   expect(scope.phones).toBeUndefined();
   httpBackend.flush();

   expect(scope.phones).toEqual([{name: 'Nexus S'},
                                 {name: 'Motorola DROID'}]);
 });

Dans le tutoriel d’Angular, il a été choisi de déclarer la variable $httpBackend, probablement pour être explicite sur la nature de l’objet (i.e. c’est le service Angular $httpBackend). Dans le beforeEach, si on continue à utiliser $httpBackend dans la fonction d’injection, alors cela conduit à l’affectation $httpBackend = $httpBackend…​Il faut donc utiliser un autre nom pour l’argument passé à la fonction d’injection : $httpBackend. Cela fonctionne parfaitement, car les caractères "" sont retirés du nom par l’injecteur d’Angular, lors de la recherche du service (cf code source injector) ⇒ le nom $httpBackend_ = le nom $httpBackend.

4- Ça marche. Une dernière question : pourquoi le service $http n’est-il pas fournit au constructeur du contrôleur dans le beforeEach ?

D’une part, on n’utilise pas $http dans le code de test pour les raisons indiquées ci-dessus. D’autre part, le service $controller qui instancie PhoneListCtrl procèdera à l’injection des services qui ne sont pas explicitement déclarés : PhoneListCtrl va donc bien recevoir $http.

C’est équivalent au code suivant, qui passe explicitement le service $http :

 var scope, ctrl, httpBackend;

 beforeEach(inject(function($httpBackend, $rootScope, $controller, $http) {
   httpBackend = $httpBackend;
   httpBackend.expectGET('phones/phones.json').
         respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);

   scope = $rootScope.$new();
   ctrl = $controller(PhoneListCtrl, {$scope: scope, $http: $http});
 }));

Step 7 - Routing & Multiple Views

  • ajouter le fichier app.js dans le répertoire projet/app/js

  • créer le répertoire partials dans projet/app et ajouter les fichiers phone-list.html et phone-detail.html

  • projet

  • app

  • index.html

  • css

  • app.css

  • bootstrap-responsive.css

  • bootstrap-responsive.min.css

  • bootstrap.css

  • bootstrap.min.css

  • img

  • un ensemble d’images…​

  • js

  • app.js

  • controllers.js

  • lib

  • angular

  • angular.js

  • partials

  • phone-detail.html

  • phone-list.html

  • phones

  • config

  • test

  • e2e

  • runner.html

  • scenarios.js

  • lib

  • angular

  • angular-scenario.js

  • angular-mocks.js

  • unit

  • controllersSpec.js

Step 9 - Filters

  • créer le fichier filters.js dans le répertoire projet/app/js

  • créer le fichier filtersSpec.js dans le répertoire test/unit

  • projet

  • app

  • index.html

  • css

  • app.css

  • bootstrap-responsive.css

  • bootstrap-responsive.min.css

  • bootstrap.css

  • bootstrap.min.css

  • img

  • un ensemble d’images…​

  • js

  • app.js

  • controllers.js

  • filters.js

  • lib

  • angular

  • angular.js

  • partials

  • phone-detail.html

  • phone-list.html

  • phones

  • config

  • test

  • e2e

  • runner.html

  • scenarios.js

  • lib

  • angular

  • angular-scenario.js

  • angular-mocks.js

  • unit

  • controllersSpec.js

  • filtersSpec.js

Step 10 - Event Handlers

Dans la description des tests unitaires pour le contrôleur PhoneDetailCtrl, il faut ajouter la propriété images, sinon le test provoque une erreur dans le contrôleur sur la ligne $scope.mainImageUrl = data.images[0]; :

 describe('PhoneDetailCtrl', function(){
   var scope, $httpBackend, ctrl;

   beforeEach(inject(function($httpBackend, $rootScope, $routeParams, $controller) {
     $httpBackend = $httpBackend;
     $httpBackend.expectGET('phones/xyz.json')
                 .respond({name:'phone xyz', images: ['image/url1.png', 'image/url2.png']});

     $routeParams.phoneId = 'xyz';
     scope = $rootScope.$new();
     ctrl = $controller(PhoneDetailCtrl, {$scope: scope});
   }));


   it('should fetch phone detail', function() {
     expect(scope.phone).toBeUndefined();
     $httpBackend.flush();

     expect(scope.phone).toEqual({name:'phone xyz', images: ['image/url1.png', 'image/url2.png']});
   });
 });

Step 11 - REST and Custom Services

  • créer le fichier services.js dans le répertoire projet/app/js

  • copier le fichier angular-resources.js du corrigé, dans le répertoire projet/app/lib/angular

  • ajouter app/lib/angular/angular-resource.js à la liste des fichiers dans testacular.conf.js

  • remplacer la vérification expect(scope.phone).toEqualData({}) par expect(scope.phone).toBeUndefined() dans controllersSpec.js :

   it('should fetch phone detail', function() {
     expect(scope.phone).toBeUndefined();
     $httpBackend.flush();

     expect(scope.phone).toEqualData(xyzPhoneData());
   });
  • projet

  • app

  • index.html

  • css

  • app.css

  • bootstrap-responsive.css

  • bootstrap-responsive.min.css

  • bootstrap.css

  • bootstrap.min.css

  • img

  • un ensemble d’images…​

  • js

  • app.js

  • controllers.js

  • filters.js

  • services.js

  • lib

  • angular

  • angular.js

  • angular-resources.js

  • partials

  • phone-detail.html

  • phone-list.html

  • phones

  • config

  • test

  • e2e

  • runner.html

  • scenarios.js

  • lib

  • angular

  • angular-scenario.js

  • angular-mocks.js

  • unit

  • controllersSpec.js

  • filtersSpec.js