Préparation aux tests unitaires d'une application AngularJS

Publié le 30 octobre 2014 par Numa Claudel | front

Cet article est publié sous licence CC BY-NC-SA

La possibilité de tester son code est un des plus grands atouts d’AngularJS. Le découplage du code en différentes parties (contrôleurs, services, directives, …) ayant chacune une mission spécifique, en facilite le test.

Deux types de tests sont à notre disposition: les tests unitaires et les tests d’intégrations dits tests End-to-End (E2E). Aujourd’hui nous allons nous intéresser à la partie tests unitaires.

Mais avant de pouvoir tester une application AngularJS il y a des prérequis, à savoir installer quelques outils et ajouter des dépendances aux modules de tests.

Les outils

J’ai sélectionné les outils qui me semble au minimum nécessaires pour qu’il soit simple de tester une application AngularJS, mais la liste n’est pas exhaustive.

Grunt pour lancer les taches de tests. Il y a de fortes chances que vous l’ayez déjà installé, mais sinon:

npm install -g grunt-cli

Les fichiers package.json et Grunfile.js seront nécessaires à la racine de votre projet pour sa configuration. Si vous utilisez un générateur tel que Yeoman, ou un framework comme Sails, ils devraient déjà être présents.

Sinon à la racine du projet:

npm init
npm install grunt --save

Voila pour le package.json. Pour le Gruntfile.js vous pouvez vous reporter à cette page.

Karma pour exécuter les suites de tests. A la base développé pour AngularJS, il a été étendu pour le test d’applications basées sur d’autres frameworks.

Pour l’installer dans votre projet:

npm install karma --save-dev

Pour générer un fichier de configuration, Karma fournit une tache d’initialisation:

node node_modules/karma/bin/karma init <fichier/de/conf>

Répondez aux questions et votre fichier de configuration sera généré.

Note: je réponds pour ma part oui à la question « Do you want Karma to watch all the files and run the tests on change ? » pour que les tests soit exécutées automatiquement au lancement de la tache de test.

Pour utiliser Karma au travers de taches Grunt ajoutez grunt-karma en dépendance de votre application:

npm install grunt-karma --save-dev

Avec, par exemple, ce début de configuration dans votre Gruntfile.js:

grunt.initConfig({
  karma: {
    unit: { configFile: 'tests/karma.conf.js' }
  }
});
grunt.loadNpmTasks('grunt-karma');

Pour ce qui est du framework de test je vous propose de regarder du coté de Jasmine. Mais il est tout à fait possible d’en utiliser un ou plusieurs autres de votre choix. Par exemple: Mocha, QUnit. Vous avez d’ailleurs eu à le choisir lors de la génération de la configuration de Karma.

ngMock est un module AngularJS à charger en plus du reste des scripts nécessaires lors des tests. Il permet d’utiliser certains modules en simulant les réponses que nous attendons pour nos tests. Par exemple il comprend le service $httpBackend qui va simuler les appels HTTP et retourner à nos tests les réponses que l’on aura définies. Pour intégrer ngMock aux tests avec Karma, commencez par l’installer:

npm install angular-mocks –save-dev
// ou avec bower
bower install angular-mocks –save-dev

Bower est un gestionnaire de paquets au même titre que npm, mais optimisé pour le front-end. Puis ajoutez angular-mocks.js à la liste des fichiers à charger dans la configuration de Karma. Exemple:

files: [
  'bower_components/angular/angular.js',
  'bower_components/angular-route/angular-route.js',
  'bower_components/angular-resource/angular-resource.js',
  'bower_components/angular-mocks/angular-mocks.js',
  'assets/js/angular/app.js',
  'assets/js/angular/**/*.js',
  'tests/unit/assets/js/angular/**/*.js'
]

Pour avoir une vision de la couverture du code par vos tests installez karma-coverage:

npm install karma-coverage --save-dev

Puis ajoutez les options dans le fichier de configuration de Karma:

preprocessors: {
  'assets/js/angular/**/*.js': ['coverage']
},

reporters: ['progress', 'coverage'],

coverageReporter: {
  type: 'html',
  dir: 'tests/coverage/'
}

A la fin de ces quelques installations un dernier npm install, et bower install si vous l’utilisez, à la racine du projet pour installer les dépendances qui peuvent être manquantes. Tout doit maintenant être prêt pour accueillir les premiers tests.

Un exemple de test unitaire

Prenons par exemple ce contrôleur:

ohMonForum.controller('PostCtrl', ['$scope', '$routeParams', 'Post',
  function($scope, $routeParams, Post) {
    // Grab all the topic's posts
    $scope.posts = Post.query({ topicId: $routeParams.id });
  }
]);

Il s’occupe d’assigner $scope.posts avec la liste que lui retourne le service Post, en lui passant préalablement l’id du topic présente dans l’url. C’est le service Post qui s’occupe de la partie requête HTTP.

Pour tester ce contrôleur on peut vérifier dans un premier temps qu’il est bien défini, puis dans un deuxième temps que le service Post et lui-même font bien leur travail. En définissant la liste des posts que l’on souhaite que Post nous retourne, on pourra ensuite tester l’état de départ de $scope.posts et son état après l’appel à Post.query:

'use strict';

describe('Controllers', function() {
  beforeEach(module('ohMonForum'));
  var $scope;

  describe('PostCtrl', function() {
    var postCtrl, $httpBackend, $routeParams;

    beforeEach(inject(function($controller, $rootScope, _$httpBackend_, _$routeParams_, Post) {
      $scope = $rootScope.$new();
      $httpBackend = _$httpBackend_;
      $routeParams = _$routeParams_;
      $routeParams.id = 1;
      postCtrl = $controller('PostCtrl', { $scope: $scope, $routeParams: $routeParams, Post: Post });
    }))

    it('should be defined', function() {
      expect(postCtrl).toBeDefined();
    });

    it('should fetch the posts', function() {
      $httpBackend.expectGET('/topic/1/posts').respond([{ "id": 1, "content": "un contenu", "topic": 1 }]);
      expect($scope.posts).toEqual(jasmine.any(Array));
      $httpBackend.flush();
      expect($scope.posts.length).toBe(1);
      expect(angular.equals($scope.posts[0], { "id": 1, "content": "un contenu", "topic": 1 })).toBeTruthy();
    });
  });
});

NgMock fournit les fonctions module et inject, ainsi que d’autres mais c’est certainement celles qui servent le plus souvent, qui permettent respectivement d’enregistrer des modules à utiliser et d’injecter des dépendances aux fonctions de tests.

On instancie donc le module global de notre application au tout début des tests dans un beforeEach, et on déclare les variables que l’on va utiliser dans les tests.

Le _$httpBackend_ et _$routeParams_ en dépendance ne sont autres que $httpBackend et $routeParams, mais inject nous permet de nommer les dépendances précédées et suivies d’underscore, pour les ignorer pendant la résolution des dépendances, et ainsi pouvoir utiliser une variable du nom de celle-ci.

On assigne ensuite les variables avec des valeurs d’initialisations:

  • assigner $scope avec un scope fraichement créé par $rootScope.$new()
  • assigner les variables avec les services qui pourront être utilisés dans les tests
  • instancier le contrôleur avec ses dépendances, que l’on a préalablement préparées

Voyez comme il est possible de définir les réponses que nous attendons avec $httpBackend sur les appels serveur. $httpBackend ne les renvoie d’ailleurs pas de lui même, il attendra un appel à la méthode flush() qui va simuler les appels serveur et retourner les réponses que l’on aura définies.

Pour lancer les tests:

grunt karma

Un navigateur devrait s’ouvrir (sauf avec PhantomJS) et jouer la suite de tests, avec un retour sur le déroulement en console. Rafraîchir la page rejoue les tests. Cliquer sur le bouton « debug » présent dans la page, ouvre un nouvel onglet dans lequel il est possible de débugger au travers de la console du navigateur.

Dans le même temps, si vous vous rendez dans tests/coverage/<navigateur utilisé>/ et que vous ouvrez index.html dans un navigateur, vous pourrez observer en couleur et en pourcentage, la couverture de tests actuelle.

Note pour finir

Une chose très importante lors du développement, c’est de ne pas manipuler le DOM n’importe ou, par exemple pas dans un contrôleur, mais seulement dans des composants dédiés à cette tache. Il sera en fait très difficile de tester du code mixant fonctionnel et manipulation de DOM.

Conclusion

Tester son code AngularJS n’est pas difficile, l’étape la plus compliquée est la préparation de l’environnement. Bien sur cela apporte une sérénité indéniable au développement, et on peut même y trouver un petit coté ludique avec la couverture de tests.

Il est toutefois primordial de bien structurer son code en respectant chaque concept, pour ne pas se retrouver avec des portions de code difficilement testable.

L’équipe Synbioz.

Libres d’être ensemble.