Blog

Les tests Angularjs : le guide de A à Z – Partie 1, les tests unitaires

Angularjs est le framework front-end à la mode en ce moment, celui que tout le monde a eu envie d’essayer au moins une fois, juste pour voir.

Chez Occitech en tout cas, on a eu envie.

Et là c’est le drame

Le framework étant tout neuf, son manque de documentation peut vite vous faire défaut. Surtout quand il s’agit de faire des tests.

Heureusement pour vous, on s’est cassé les dents à votre place, et comme nous sommes de braves gens, on va tout vous expliquer du début à la fin.

La première chose à savoir, c’est qu’Angularjs met deux types de tests à notre disposition : des tests unitaires, et des tests dits end to end, qui sont en fait des tests fonctionnels. Dans cette première partie, on va se concentrer sur les tests unitaires.

Mais avant de rentrer dans le détail des tests, commençons par installer les outils dont nous aurons besoin pour exécuter les tests.

Karma

La principale chose à utiliser est karma (anciennement nommé testacular, on se demande pourquoi le nom a été changé…), qui a été créé spécialement pour les tests d’Angularjs au départ, mais qui est capable de tester n’importe quel type de projet javascript.

Installation

Rien de plus simple grâce à notre ami npm :

$ npm install --save-dev karma

Ensuite, pour les tests unitaires, nous aurons besoin de jasmine :

$ npm install --save-dev karma-jasmine

Et enfin nous utiliserons PhantomJs comme navigateur exécutant les tests. Il faudra donc ajouter le launcher adapté pour Karma :

$ npm install --save-dev karma-phantomjs-launcher

Maintenant qu’on a karma et ses dépendances pour Angularjs, on va pouvoir entrer dans le vif du sujet et commencer nos tests !

Les tests unitaires

Partons du code suivant :

// js/test_app.js
angular.module('testApp', [])
  .controller('FooController', ['$scope', '$http', '$location', function($scope, $http, $location) {
    $scope.model = {};

    $scope.save = function(model) {
    $http.post('/api/foo.json', model).success(function() {
      $location.url('/redirect/to/url');
    });
  };
}])
;

Quels sont les tests unitaires que nous pourrions appliquer au contrôleur FooController ? Il y en a trois qui sont évidents :

  • $scope.save doit être défini
  • Si on fait appel à la fonction $scope.save(), on doit faire un appel POST à l’URL ‘/api/foo.json’.
  • Si l’appel POST à ‘/api/foo.json’ s’est bien passé, la méthode $location.url() doit être appelée avec ‘/redirect/to/url’ en paramètre.

Empressons-nous de mettre ces tests en place !

Première étape : configuration de karma

Eh oui, il faut d’abord dire à karma comment il doit être exécuté pour des tests unitaires Angularjs.

Rien de bien compliqué, avec la doc. du site officiel et angular-seed, on peut rapidement trouver comment faire :

// karma-unit.conf.js
module.exports = function(config) {
  config.set({
    basePath: './',
    files: [
      'bower_components/angular/angular.js',
      'bower_components/angular-mocks/angular-mocks.js',
      'js/test_app.js',
      'test/unit/**/*.js'
    ],
    frameworks: ['jasmine'],
    autoWatch: true,
    browsers: ['PhantomJS']
  });
};

Que fait-on ?

  • basePath : on indique à karma que notre répertoire de travail est celui dans lequel se trouve le fichier de configuration
  • files : il s’agit d’un tableau de tous les fichiers devant être inclus lors de l’exécution des tests. On retrouve donc, les sources Angularjs, la librairie de mocks d’Angularjs, notre application Angularjs et tous les fichiers .js se trouvant dans test/unit/
  • framework : un tableau des frameworks de tests à utiliser. Ici uniquement jasmine qui est le framework des tests unitaires d’Angularjs
  • autoWatch : on ré-exécute les tests à chaque fois qu’un fichier présent dans files est modifié
  • browsers : un tableau des navigateurs que l’on souhaite utiliser pour faire les tests (afin de pouvoir tester avec différents moteurs javascript par exemple). Ici nous n’utiliserons que PhantomJS par simplicité (on pourrait aussi utiliser un vrai Google Chrome qui écoute une URL en continu, mais c’est un peu lourd à mon goût :().

Écrire un premier test

// test/unit/foo_controller.spec.js
describe('FooController', function() {
  var $scope, controller;

  beforeEach(module('testApp'));
  beforeEach(inject(function ($rootScope, $controller) {
    $scope = $rootScope.$new();
    controller = $controller('FooController', {
      $scope: $scope
    });
  }));

  it('should set save function', function() {
    expect($scope.save).toBeDefined();
  });
});

On ne va pas entrer en détail dans toute la partie beforeEach, ce serait trop long (reportez-vous à la doc. — haha — ou à différentes ressources sur le web qui vous expliqueront tout ça mieux que nous), et se focaliser sur notre test.

Jusque là, rien de bien compliqué, le test fait ce qu’il dit et vérifie que la variable $scope.save a bien été définie.

Lançons notre test :

$ node_modules/.bin/karma start karma-unit.conf.js --single-run
INFO [karma]: Karma v0.10.2 server started at http://localhost:9876/
INFO [launcher]: Starting browser PhantomJS
INFO [PhantomJS 1.9.2 (Mac OS X)]: Connected on socket FUsDNFuXaM_LyVGxmJTs
PhantomJS 1.9.2 (Mac OS X): Executed 1 of 1 SUCCESS (0.09 secs / 0.01 secs)

Fantastique, c’est un succès !

Le second test

Là, ça se corse un peu. On veut tester qu’une URL est bien appelée lors de l’appel à la fonction $scope.save(). Et pour ça, il n’y a pas trente-six solutions, il faut mocker !

La fonction inject() de la librairie de mocks d’Angularjs va nous permettre, comme son nom l’indique, d’injecter des modules mockés au sein de nos tests :

// test/unit/foo_controller.spec.js
describe('FooController', function() {
  //…
  it('should call /api/foo.json on $scope.save()', inject(function($httpBackend) {
    $scope.save();

    $httpBackend.expectPOST('/api/foo.json').respond();
    $httpBackend.flush();
  }));
});

Ici par exemple, on se sert du ngMock.$httpBackend, qui nous permet d’indiquer qu’un appel POST à ‘/api/foo.json’ est attendu.

L’appel à respond() permet de simuler la réponse de notre mock. Puis on dit au mock $httpBackend de flusher les requêtes en attente (puisque normalement ce sont des requêtes asynchrones).

On lance nos tests :

node_modules/.bin/karma start karma-unit.conf.js --single-run
INFO [karma]: Karma v0.10.2 server started at http://localhost:9876/
INFO [launcher]: Starting browser PhantomJS
INFO [PhantomJS 1.9.2 (Mac OS X)]: Connected on socket 3N5hoDfNmHw3UcqZnLRc
PhantomJS 1.9.2 (Mac OS X): Executed 2 of 2 SUCCESS (0.096 secs / 0.013 secs)

Encore une fois c’est un succès. :)

Le troisième et dernier test

On doit maintenant vérifier que $location.url() est appelée en cas de succès de l’appel API et qu’il doit avoir pour paramètre ‘/redirect/to/url’.

Pour faire ça, rien de plus simple, on va utiliser les mocks de Jasmine.

// test/unit/foo_controller.spec.js
describe('FooController', function() {
  //…
  beforeEach(inject(function ($rootScope, $controller) {
    //…
    $location = jasmine.createSpyObj('$location', ['url']);
    controller = $controller('FooController', {
      $scope: $scope,
      $location: $location
    });
  }));
  //…
  it('should redirect to /redirect/to/url', inject(function($httpBackend) {
    $scope.save();
    $httpBackend.whenPOST('/api/foo.json').respond(200);
    $httpBackend.flush();

    expect($location.url).toHaveBeenCalledWith('/redirect/to/url');
  }));
});

Ce qu’on a rajouté :

  • Dans le beforeEach, on crée un mock de $location et on le passe en paramètre au controller
  • On fait un stub sur $httpBackend pour retourner le code HTTP 200 en cas d’appel POST à ‘/api/foo.json’
  • On s’assure que $location.url() a été appelée avec ‘/redirect/to/url’ en paramètre

On exécute les tests :

node_modules/.bin/karma start karma-unit.conf.js --single-run
INFO [karma]: Karma v0.10.2 server started at http://localhost:9876/
INFO [launcher]: Starting browser PhantomJS
INFO [PhantomJS 1.9.2 (Mac OS X)]: Connected on socket 47n3Fbu5OR4m2oHsqdbB
PhantomJS 1.9.2 (Mac OS X): Executed 3 of 3 SUCCESS (0.098 secs / 0.013 secs)

Et voilà, tous les tests passent !

Conclusion

Pour conclure, on peut dire que les tests Angularjs, c’est pas si compliqué que ça, mais le manque de documentation « officielle » est quand même un gros défaut. Et vouloir se lancer dedans tout seul peut parfois faire peur et n’est pas forcément évident non plus.

Par contre une fois qu’on a chopé le coup, on peut tout tester sans plus jamais avoir peur de rien !

Tester le code de ce billet

Parce qu’à Occitech, on est vraiment des gars sympas, on vous a préparé un dépôt Github avec tout le code décrit ici prêt (ou presque) à être exécuté.

Pour ce faire, rendez-vous ici : https://github.com/occitech/angularjs-testing.