Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angularjs promise not being resolved in unit test

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/

like image 498
robbymurphy Avatar asked Feb 24 '13 02:02

robbymurphy


2 Answers

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;
    });
}
like image 177
pkozlowski.opensource Avatar answered Oct 23 '22 04:10

pkozlowski.opensource


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();
}));
like image 8
Kevin McCloskey Avatar answered Oct 23 '22 04:10

Kevin McCloskey