30 Oct 2014

Tester un controller angularJS faisant appel à une méthode de service renvoyant une promise

Dans le cadre d’un des projets de l’équipe utilisant AngularJS, nous avons implémenté des tests unitaires sur le code client.

Rapidement, nous avons été confrontés à une problématique sur un controller qui lors de son initialisation fait appel à un service afin d’alimenter une propriété sur le scope :


// … Code supprimé pour plus de lisibilité
$scope.services = [];
mosaiqueService.getMesServices().then(function (result) {
$scope.services = result.data.attachments;
});
// … Code supprimé pour plus de lisibilité

La méthode du service renvoie une promise. Notre premier réflexe a été d’implémenter le test suivant afin de nous assurer que les données étaient bien chargées à l’initialisation du controller :

describe('errorControllerTests', function () {
var $scope, createController, mosaiqueService;
beforeEach(module('notre.module'));
beforeEach(inject(function ($rootScope, $controller, $q) {
var mesServices = $q.when({ attachments: [{ id: 1, label: 'service 1' }] });
mosaiqueService=jasmine.createSpyObj('mosaiqueService', ['getMesServices']);
mosaiqueService.getMesServices.and.returnValue(mesServices);
$scope = $rootScope.$new();
createController = function () {
return $controller('mosaiqueController', {
'$scope': $scope,
'mosaiqueService': mosaiqueService
});
};
}));
describe('initialisation test', function () {
it('doit initialiser la liste des services sur $scope', function () {
createController();
expect($scope.services).toBeDefined();
expect($scope.services.length).toEqual(1);
expect(mosaiqueService.getMesServices).toHaveBeenCalled();
expect(mosaiqueService.getMesServices.calls.count()).toEqual(1);
});
});
});

En réalité, ce test échoue. En positionnant un point d’arrêt au niveau des assertions, nous nous sommes rendus compte que la propriété « services » de l’objet $scope restait undefined.
Après quelques recherches, nous nous sommes rendus compte qu’une promesse n’est résolue que lors de la phase angular $digest. Lors de l’exécution d’une application angular, le framework gère tout cela de manière transparente pour nous. Par contre, dans le cadre des tests unitaires c’est à nous de gérer les phases $digest. C’est pourquoi, il nous a fallu ajouter l’instruction $rootScope.$apply avant de pouvoir effectuer nos vérification sur les données positionnées dans $scope :


describe('initialisation test', function () {
it('doit initialiser la liste des services sur $scope', function () {
createController();
$rootScope.$apply();
expect($scope.services).toBeDefined();
expect($scope.services.length).toEqual(1);
expect(mosaiqueService.getMesServices).toHaveBeenCalled();
expect(mosaiqueService.getMesServices.calls.count()).toEqual(1);
});
});

En effet, en interne $rootScope.$apply() appelle $rootScope.$digest().

Share
  • Kogratte

    N’aurait-il pas été ici plus pratique de se désintéresser du “comment” les services ont été récupérés? Je veux dire par la, qu’en se contentant de vérifier que la liste est bien remplie après le apply, on se détache de la méthode pour arriver au résultat.

    Si demain on change la méthode de récupération, le test reste vrai, le controlleur rempli toujours son rôle.