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épertoireapp/lib/angular
-
créer le fichier
controllers.js
dans le répertoirejs
-
créer le répertoire
test/unit
-
créer le fichier
controllersSpec.js
dans le répertoiretest/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 commandetestacular 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épertoireconfig
-
modifier le
basePath
du fichiertestacular.conf.js
:basePath = '../';
-
démarrer le serveur de test dans une seconde console, depuis le répertoire
config
avec la commandetestacular start
(le serveur peut être arrêté avec unctrl 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
etcss
dans le répertoireapp
-
créer le répertoire
e2e
dans le répertoiretest
-
créer le fichier
scenarios.js
dans le répertoiree2e
-
copier le fichier
runner.html
dans le répertoiree2e
depuis le projet corrigé -
créer le répertoire
lib/angular
dans le répertoiretest
-
copier le fichier
angular-scenario.js
dans le répertoiretest/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épertoiretest/lib/angular
du projet -
ajouter
angular-mocks.js
dans la liste des fichiers detestacular.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épertoireprojet/app/js
-
créer le répertoire
partials
dansprojet/app
et ajouter les fichiersphone-list.html
etphone-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épertoireprojet/app/js
-
créer le fichier
filtersSpec.js
dans le répertoiretest/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épertoireprojet/app/js
-
copier le fichier
angular-resources.js
du corrigé, dans le répertoireprojet/app/lib/angular
-
ajouter
app/lib/angular/angular-resource.js
à la liste des fichiers danstestacular.conf.js
-
remplacer la vérification
expect(scope.phone).toEqualData({})
parexpect(scope.phone).toBeUndefined()
danscontrollersSpec.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