AngularJS/tutorial

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 :

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 :

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 :

Étapes du tutoriel

Step 1 - Static Template

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

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 :

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

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 :



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 :

 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 :

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



Step 9 - Filters


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

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