Angularjs promise not being resolved in unit test
Solution 1
I guess that the key to this "mystery" is the fact that AngularJS will automatically resolve promises (and render results) if those used in an interpolation directive in a template. What I mean is that given this controller:
MyCtrl = function($scope, $http) {
$scope.promise = $http.get('myurl', {..});
}
and the template:
<span>{{promise}}</span>
AngularJS, upon $http call completion, will "see" that a promise was resolved and will re-render template with the resolved results. This is what is vaguely mentioned in the $q documentation:
$q promises are recognized by the templating engine in angular, which means that in templates you can treat promises attached to a scope as if they were the resulting values.
The code where this magic happens can be seen here.
BUT, this "magic" happens only when there is a template ($parse
service, to be more precise) at play. In your unit test there is no template involved so promise resolution is not propagated automatically.
Now, I must say that this automatic resolution / result propagation is very convenient but might be confusing, as we can see from this question. This is why I prefer to explicitly propagate resolution results as you did:
var MyController = function($scope, service) {
service.getStuff().then(function(result) {
$scope.myVar = result;
});
}
Solution 2
I had a similar problem and left my controller assigning $scope.myVar directly to the promise. Then in the test, I chained on another promise that asserts the expected value of the promise when it gets resolved. I used a helper method like this:
var expectPromisedValue = function(promise, expectedValue) {
promise.then(function(resolvedValue) {
expect(resolvedValue).toEqual(expectedValue);
});
}
Note that depending on the ordering of when you call expectPromisedValue and when the promise is resolved by your code under test, you may need to manually trigger a final digest cycle to run in order to get these then() methods to fire - without it your test may pass regardless of whether the resolvedValue
equals the expectedValue
or not.
To be safe, put the trigger in an afterEach() call so you don't have to remember it for every test:
afterEach(inject(function($rootScope) {
$rootScope.$apply();
}));
Solution 3
@pkozlowski.opensource answered the why (THANK YOU!), but not how to get around it in testing.
The solution I just arrived at is to test that HTTP is getting called in the service, and then spy on the service methods in the controller tests and return actual values instead of promises.
Suppose we have a User service that talks to our server:
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
},
// ...
};
});
Testing that service, we don't care what happens to the returned values, only that the HTTP calls were made correctly.
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()
Now in our controller tests, there's no need to worry about HTTP. We only want to see that the User service is being put to work.
angular.module('app.controllers')
.controller('UserCtrl', function ($scope, $routeParams, User) {
$scope.user = User.get($routeParams.handle);
});
To test this, we spy on the User service.
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';
No need to resolve the promises. Hope this helps.
Comments
-
robbymurphy almost 2 years
I am using jasmine to unit test an angularjs controller that sets a variable on the scope to the result of calling a service method that returns a promise object:
var MyController = function($scope, service) { $scope.myVar = service.getStuff(); }
inside the service:
function getStuff() { return $http.get( 'api/stuff' ).then( function ( httpResult ) { return httpResult.data; } ); }
This works fine in the context of my angularjs application, but does not work in the jasmine unit test. I have confirmed that the "then" callback is executing in the unit test, but the $scope.myVar promise never gets set to the return value of the callback.
My unit test:
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" ); } ); } );
Also, if I change the controller to the following the unit test passes:
var MyController = function($scope, service) { service.getStuff().then(function(result) { $scope.myVar = result; }); }
Why is the promise callback result value not being propagated to $scope.myVar in the unit test? See the following jsfiddle for full working code http://jsfiddle.net/s7PGg/5/
-
blaster about 11 yearsIn that example where you're testing the service, we had to add the following before calling $httpBackend.flush(): $rootScope.$apply()
-
Gepsens about 11 yearsIf you are mocking the back end like I am, result will be a composite with a property "data" holding the actual response content.
-
Christian Smith almost 11 years@blaster ... then you're doing something wrong. Services should be tested outside the context of controllers and scope.
-
zhon over 9 yearsIn Angular 1.2, promises are no longer automatically resolved (AKA, unwrapped).