Angularjs promet de ne pas être résolu en test unitaire

J'utilise jasmine pour tester l'unité d'un contrôleur angularjs qui définit une variable sur la portée du résultat d'appel d'une méthode de service qui renvoie un objet prometteur:

var MyController = function($scope, service) { $scope.myVar = service.getStuff(); } 

À l'intérieur du service:

 function getStuff() { return $http.get( 'api/stuff' ).then( function ( httpResult ) { return httpResult.data; } ); } 

Cela fonctionne bien dans le contexte de mon application angularjs, mais ne fonctionne pas dans le test de l'unité de jasmin. J'ai confirmé que le rappel "alors" s'exécute dans le test de l'unité, mais la promesse $ scope.myVar ne se règle jamais sur la valeur de retour du rappel.

Mon test unitaire:

 describe( 'My Controller', function () { var scope; var serviceMock; var controller; var httpBackend; beforeEach( inject( function ( $rootScope, $controller, $httpBackend, $http ) { scope = $rootScope.$new(); httpBackend = $httpBackend; serviceMock = { stuffArray: [{ FirstName: "Robby" }], getStuff: function () { return $http.get( 'api/stuff' ).then( function ( httpResult ) { return httpResult.data; } ); } }; $httpBackend.whenGET( 'api/stuff' ).respond( serviceMock.stuffArray ); controller = $controller( MyController, { $scope: scope, service: serviceMock } ); } ) ); it( 'should set myVar to the resolved promise value', function () { httpBackend.flush(); scope.$root.$digest(); expect( scope.myVar[0].FirstName ).toEqual( "Robby" ); } ); } ); 

En outre, si je change le contrôleur à la suite, le test d'unité passe:

 var MyController = function($scope, service) { service.getStuff().then(function(result) { $scope.myVar = result; }); } 

Pourquoi la valeur du résultat de rappel de la promesse n'est pas propagée à $ scope.myVar dans le test de l'unité? Voir le jsfiddle suivant pour le code de travail complet http://jsfiddle.net/s7PGg/5/

Je suppose que la clé de ce «mystère» est que AngularJS résoudra automatiquement les promesses (et les résultats de rendu) si ceux utilisés dans une directive d'interpolation dans un modèle. Ce que je veux dire, c'est que compte tenu de ce contrôleur:

 MyCtrl = function($scope, $http) { $scope.promise = $http.get('myurl', {..}); } 

Et le modèle:

 <span>{{promise}}</span> 

AngularJS, après l'achèvement de l'achat de $ $, "verra" qu'une promesse a été résolue et renumera le modèle avec les résultats résolus. C'est ce qui est vaguement mentionné dans la documentation $ q :

Les promesses de $ q sont reconnues par le moteur de modèle angulaire, ce qui signifie que dans les modèles, vous pouvez traiter les promesses liées à une portée comme si elles étaient les valeurs résultantes.

Le code où se produit cette magie peut être vu ici .

MAIS, cette "magie" ne se produit que s'il existe un modèle ( $parse service, pour être plus précis) en jeu. Dans votre test d'unité, il n'y a pas de modèle impliqué, donc la résolution de la promesse n'est pas propagée automatiquement .

Maintenant, je dois dire que cette propagation automatique de résolution / résultat est très pratique, mais pourrait être source de confusion, comme on peut le voir à partir de cette question. C'est pourquoi je préfère propager explicitement les résultats de résolution comme vous l'avez fait:

 var MyController = function($scope, service) { service.getStuff().then(function(result) { $scope.myVar = result; }); } 

J'ai eu un problème similaire et j'ai laissé mon contrôleur attribuer $ scope.myVar directement à la promesse. Ensuite, dans le test, je suis enchaîné sur une autre promesse qui affirme la valeur attendue de la promesse lorsqu'elle est résolue. J'ai utilisé une méthode d'aide comme celle-ci:

 var expectPromisedValue = function(promise, expectedValue) { promise.then(function(resolvedValue) { expect(resolvedValue).toEqual(expectedValue); }); } 

Notez que, en fonction de la commande de lorsque vous appelez expectPromisedValue et lorsque la promesse est résolue par votre code sous test, vous devrez peut-être déclencher manuellement un cycle de synthèse finale pour exécuter afin que ces méthodes () Le test peut passer indépendamment du fait que le resolvedValue soit égal ou non à la valeur expectedValue .

Pour être sûr, placez la gâchette dans un appel afterEach () afin que vous ne devez pas le rappeler pour chaque test:

 afterEach(inject(function($rootScope) { $rootScope.$apply(); })); 

@ Pkozlowski.opensource a répondu au pourquoi (MERCI!), Mais pas à la façon de le contourner dans les tests.

La solution à laquelle je viens d'arriver est de tester que HTTP est appelé dans le service, puis d'espionner les méthodes de service dans les tests du contrôleur et de renvoyer les valeurs réelles plutôt que les promesses.

Supposons que nous ayons un service utilisateur qui parle à notre serveur:

 var services = angular.module('app.services', []); services.factory('User', function ($q, $http) { function GET(path) { var defer = $q.defer(); $http.get(path).success(function (data) { defer.resolve(data); } return defer.promise; } return { get: function (handle) { return GET('/api/' + handle); // RETURNS A PROMISE }, // ... }; }); 

En testant ce service, nous ne nous soucions pas de ce qui arrive aux valeurs retournées, seulement que les appels HTTP ont été effectués correctement.

 describe 'User service', -> User = undefined $httpBackend = undefined beforeEach module 'app.services' beforeEach inject ($injector) -> User = $injector.get 'User' $httpBackend = $injector.get '$httpBackend' afterEach -> $httpBackend.verifyNoOutstandingExpectation() $httpBackend.verifyNoOutstandingRequest() it 'should get a user', -> $httpBackend.expectGET('/api/alice').respond { handle: 'alice' } User.get 'alice' $httpBackend.flush() 

Maintenant, dans nos tests de contrôleur, il n'est pas nécessaire de s'inquiéter de HTTP. Nous voulons seulement voir que le service utilisateur est mis en oeuvre.

 angular.module('app.controllers') .controller('UserCtrl', function ($scope, $routeParams, User) { $scope.user = User.get($routeParams.handle); }); 

Pour tester cela, nous espions le service utilisateur.

 describe 'UserCtrl', () -> User = undefined scope = undefined user = { handle: 'charlie', name: 'Charlie', email: '[email protected]' } beforeEach module 'app.controllers' beforeEach inject ($injector) -> # Spy on the user service User = $injector.get 'User' spyOn(User, 'get').andCallFake -> user # Other service dependencies $controller = $injector.get '$controller' $routeParams = $injector.get '$routeParams' $rootScope = $injector.get '$rootScope' scope = $rootScope.$new(); # Set up the controller $routeParams.handle = user.handle UserCtrl = $controller 'UserCtrl', $scope: scope it 'should get the user by :handle', -> expect(User.get).toHaveBeenCalledWith 'charlie' expect(scope.user.handle).toBe 'charlie'; 

Pas besoin de résoudre les promesses. J'espère que cela t'aides.