Unit test service that returns promise Angularjs Jasmine

28,934

To mock a function that returns a promise, it will need to also return a promise, which then needs to be resolved as a separate step.

In your case the deferred.resolve() you pass to the spy needs to be replaced with deferred.promise, and the deferred.resolve() performed separately.

beforeEach(function() {
  var deferred = $q.defer();
  spyOn(dialogFactory, "confirmDeleteDialog").and.returnValue(deferred.promise);
  spyOn(requestNotificationChannel, "deleteMessage");
  $scope.deleteMessage(5);
  deferred.resolve();
  $rootScope.$digest();
});

it("delete message notification is called", function() {
  expect(requestNotificationChannel.deleteMessage).toHaveBeenCalled();
});

I suspect you also need to call $rootScope.$digest(), as Angular's promise implementation is tied to the digest loop.

Also, slightly unrelated to your question, but I don't think you need to create your own deferred object in confirmDeleteDialog. The (anti) pattern you're using has been labelled 'The Forgotten Promise', as in http://taoofcode.net/promise-anti-patterns/

When is simpler, uses less code, and I think that allows better error handling, you can just return the promise that the $modal service creates:

var modalInstance = $modal.open({...});
return modalInstance.result;

If you want to modify what the calling function sees, in terms of resolved/rejected values, you can create a chained promise by returning the result of then:

var modalInstance = $modal.open({...});
return modalInstance.result.then(function(successResult) {
  return 'My other success result';
}, function(failureReason) {
  return $q.reject('My other failure reason');
});

You would usually want to do this if you don't want to expose the inner-workings of a function to its caller. This is analogous to the concept of re-throwing an exception in synchronous programming.

Share:
28,934

Related videos on Youtube

Mdb
Author by

Mdb

Updated on April 07, 2020

Comments

  • Mdb
    Mdb over 4 years

    EDITED per Michal Charemza post.

    I have a service that represents angularui modal dialog:

    app.factory("dialogFactory", function($modal, $window, $q) {
    
        function confirmDeleteDialog() {
    
        var modalInstance = $modal.open({
            templateUrl: "../application/factories/confirmDeleteDialog.htm",
            controller: function($scope, $modalInstance) {
    
                $scope.ok = function() {
                    $modalInstance.close("true");
                };
    
                $scope.cancel = function() {
                    $modalInstance.dismiss("false");
                };
            }
        });
    
    
        return modalInstance.result.then(function(response) {
            return 'My other success result';
        }, function(response) {
            return $q.reject('My other failure reason');
        });
    
    };
    
        return {
            confirmDeleteDialog: confirmDeleteDialog
        };
    
    });
    

    On calling the delete method if the user has clicked Ok from the dialog requestNotificationChannel.deleteMessage(id) is executed.

    $scope.deleteMessage = function(id) {
            var result = dialogFactory.confirmDeleteDialog();
    
            result.then(function(response) {
                requestNotificationChannel.deleteMessage(id);
            });
        };
    

    The problem is I am not able to unit test this.

    This is my test. I have correctly injected the q service but I am not sure what should I return from "confirmDeleteDialog" spy...

    describe("has a delete method that should call delete message notification", function() {
                var deferred = $q.defer();
                spyOn(dialogFactory, "confirmDeleteDialog").and.returnValue(deferred.promise);
    
                spyOn(requestNotificationChannel, "deleteMessage");
    
                $scope.deleteMessage(5);
                deferred.resolve();
    
                it("delete message notification is called", function() {
                    expect(requestNotificationChannel.deleteMessage).toHaveBeenCalled();
                });
            });
    

    But I am receiving expected spy deleteMessage to have been called. Which means that the result.then... part is not executed. What am I missing ?

  • Michal Charemza
    Michal Charemza over 10 years
    I've added a part about $rootScope.$digest()
  • Benjamin Gruenbaum
    Benjamin Gruenbaum over 10 years
    Yes, $digest() is definitely required here.