How do I mock a service that returns promise in AngularJS Jasmine unit test?

153,128

Solution 1

I'm not sure why the way you did it doesn't work, but I usually do it with the spyOn function. Something like this:

describe('Testing remote call returning promise', function() {
  var myService;

  beforeEach(module('app.myService'));

  beforeEach(inject( function(_myService_, myOtherService, $q){
    myService = _myService_;
    spyOn(myOtherService, "makeRemoteCallReturningPromise").and.callFake(function() {
        var deferred = $q.defer();
        deferred.resolve('Remote call result');
        return deferred.promise;
    });
  }

  it('can do remote call', inject(function() {
    myService.makeRemoteCall()
      .then(function() {
        console.log('Success');
      });    
  }));

Also remember that you will need to make a $digest call for the then function to be called. See the Testing section of the $q documentation.

------EDIT------

After looking closer at what you're doing, I think I see the problem in your code. In the beforeEach, you're setting myOtherServiceMock to a whole new object. The $provide will never see this reference. You just need to update the existing reference:

beforeEach(inject( function(_myService_, $q){
    myService = _myService_;
    myOtherServiceMock.makeRemoteCallReturningPromise = function() {
        var deferred = $q.defer();
        deferred.resolve('Remote call result');
        return deferred.promise;   
    };
  }

Solution 2

We can also write jasmine's implementation of returning promise directly by spy.

spyOn(myOtherService, "makeRemoteCallReturningPromise").andReturn($q.when({}));

For Jasmine 2:

spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue($q.when({}));

(copied from comments, thanks to ccnokes)

Solution 3

describe('testing a method() on a service', function () {    

    var mock, service

    function init(){
         return angular.mock.inject(function ($injector,, _serviceUnderTest_) {
                mock = $injector.get('service_that_is_being_mocked');;                    
                service = __serviceUnderTest_;
            });
    }

    beforeEach(module('yourApp'));
    beforeEach(init());

    it('that has a then', function () {
       //arrange                   
        var spy= spyOn(mock, 'actionBeingCalled').and.callFake(function () {
            return {
                then: function (callback) {
                    return callback({'foo' : "bar"});
                }
            };
        });

        //act                
        var result = service.actionUnderTest(); // does cleverness

        //assert 
        expect(spy).toHaveBeenCalled();  
    });
});

Solution 4

You can use a stubbing library like sinon to mock your service. You can then return $q.when() as your promise. If your scope object's value comes from the promise result, you will need to call scope.$root.$digest().

var scope, controller, datacontextMock, customer;
  beforeEach(function () {
        module('app');
        inject(function ($rootScope, $controller,common, datacontext) {
            scope = $rootScope.$new();
            var $q = common.$q;
            datacontextMock = sinon.stub(datacontext);
            customer = {id:1};
           datacontextMock.customer.returns($q.when(customer));

            controller = $controller('Index', { $scope: scope });

        })
    });


    it('customer id to be 1.', function () {


            scope.$root.$digest();
            expect(controller.customer.id).toBe(1);


    });

Solution 5

using sinon :

const mockAction = sinon.stub(MyService.prototype,'actionBeingCalled')
                     .returns(httpPromise(200));

Known that, httpPromise can be :

const httpPromise = (code) => new Promise((resolve, reject) =>
  (code >= 200 && code <= 299) ? resolve({ code }) : reject({ code, error:true })
);
Share:
153,128

Related videos on Youtube

Georgii Oleinikov
Author by

Georgii Oleinikov

Updated on July 08, 2022

Comments

  • Georgii Oleinikov
    Georgii Oleinikov almost 2 years

    I have myService that uses myOtherService, which makes a remote call, returning promise:

    angular.module('app.myService', ['app.myOtherService'])
      .factory('myService', [
        myOtherService,
        function(myOtherService) {
          function makeRemoteCall() {
            return myOtherService.makeRemoteCallReturningPromise();
          }
    
          return {
            makeRemoteCall: makeRemoteCall
          };      
        }
      ])
    

    To make a unit test for myService I need to mock myOtherService, such that its makeRemoteCallReturningPromise method returns a promise. This is how I do it:

    describe('Testing remote call returning promise', function() {
      var myService;
      var myOtherServiceMock = {};
    
      beforeEach(module('app.myService'));
    
      // I have to inject mock when calling module(),
      // and module() should come before any inject()
      beforeEach(module(function ($provide) {
        $provide.value('myOtherService', myOtherServiceMock);
      }));
    
      // However, in order to properly construct my mock
      // I need $q, which can give me a promise
      beforeEach(inject(function(_myService_, $q){
        myService = _myService_;
        myOtherServiceMock = {
          makeRemoteCallReturningPromise: function() {
            var deferred = $q.defer();
    
            deferred.resolve('Remote call result');
    
            return deferred.promise;
          }    
        };
      }
    
      // Here the value of myOtherServiceMock is not
      // updated, and it is still {}
      it('can do remote call', inject(function() {
        myService.makeRemoteCall() // Error: makeRemoteCall() is not defined on {}
          .then(function() {
            console.log('Success');
          });    
      }));  
    

    As you can see from the above, the definition of my mock depends on $q, which I have to load using inject(). Furthermore, injecting the mock should be happening in module(), which should be coming before inject(). However, the value for the mock is not updated once I change it.

    What is the proper way to do this?

    • dnc253
      dnc253 almost 10 years
      Is the error really on myService.makeRemoteCall()? If so, the problem is with myService not having the makeRemoteCall, not anything to do with your mocked myOtherService.
    • Georgii Oleinikov
      Georgii Oleinikov almost 10 years
      The error is on myService.makeRemoteCall(), because myService.myOtherService is just an empty object at this point (its value was never updated by angular)
    • twDuke
      twDuke over 8 years
      You add the empty object to the ioc container, after that you change the reference myOtherServiceMock to point to a new object which you spy on. Whats in the ioc container wont reflect that, as the reference is changed.
  • Priya Ranjan Singh
    Priya Ranjan Singh over 9 years
    And you killed me yesterday by not showing up in results. Beautiful display of andCallFake(). Thank you.
  • Jordan Running
    Jordan Running over 9 years
    Instead of andCallFake you can use andReturnValue(deferred.promise) (or and.returnValue(deferred.promise) in Jasmine 2.0+). You need to define deferred before you call spyOn, of course.
  • ccnokes
    ccnokes over 9 years
    Note to people using Jasmine 2.0, .andReturn() has been replaced by .and.returnValue. So the above example would be: spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue($q.when({}‌​)); I just killed a half hour figuring that out.
  • Darren Corbett
    Darren Corbett about 9 years
    This is how I have done it in the past. Create a spy that returns a fake that mimics the "then"
  • Rob Paddock
    Rob Paddock about 9 years
    Can you provide an example of the complete test you have. I have a similar problem of having a service that returns a promise, but with in it also makes a call which returns a promise !
  • Darren Corbett
    Darren Corbett about 9 years
    Hi Rob, not sure why you would want to mock a call that a mock makes to another service surely you would want to test that when testing that function. If the function calls you are mocking calls a service gets data then affects that data your mocked promise would return a fake affected data set, at least that's how I would do it.
  • Jim Aho
    Jim Aho almost 9 years
    How would you call $digest in this case when you don't have access to the scope?
  • dnc253
    dnc253 almost 9 years
    @JimAho Typically you just inject $rootScope and call $digest on that.
  • Jim Aho
    Jim Aho almost 9 years
    @dnc253 Yes thanks! I just found that out on this page: docs.angularjs.org/api/ng/service/$q. I totally had missed the integration of $q with the angular scoping mechanisms.
  • Darren Corbett
    Darren Corbett almost 9 years
    The whole point of a before each is that it is called before each test I don't know how you write your tests but personally I write multiple tests for a single function, therefore I would have a common base set up that would be called before each test. Also you may want to look up the understood meaning of anti pattern as it associates to software engineering.
  • Mark Nadig
    Mark Nadig over 8 years
    I started down this path and it works great for simple scenarios. I even created a mock that simulates chaining and provides "keep"/"break" helpers to invoke the chain gist.github.com/marknadig/c3e8f2d3fff9d22da42b In more complex scenarios, this falls down however. In my case, I had a service that would conditionally return items from a cache (w/ deferred) or make a request. So, it was creating it's own promise.
  • Mark Nadig
    Mark Nadig over 8 years
    This is the approach I ended up with. However, I wrapped the construction of the promise in a helper function. It handles inject($q, $rootScope), $q.defer and then decorates the resulting promise with a "keep" method that calls resolve and $rootScope.digest.
  • Custodio
    Custodio over 8 years
    This post ng-learn.org/2014/08/Testing_Promises_with_Jasmine_Provide_S‌​py describes the usage of fake "then" throughly.
  • ATrubka
    ATrubka almost 8 years
    $digest is most commonly forgotten step.
  • fodma1
    fodma1 over 7 years
    Using deferred in this case is unnecessary. You can just use $q.when() codelord.net/2015/09/24/$q-dot-defer-youre-doing-it-wrong
  • Admin
    Admin almost 7 years
    this is the missing piece, calling $rootScope.$digest() to get the promise to be resolved
  • Ajay.k
    Ajay.k over 5 years
    @dnc253: I have an issue to get data from controller can you please look into it. Can you help me on it. stackoverflow.com/questions/53629138/…