157

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?

Sebastian Simon
  • 18,263
  • 7
  • 55
  • 75
Georgii Oleinikov
  • 3,865
  • 3
  • 27
  • 27
  • 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`. – dnc253 May 16 '14 at 22:12
  • The error is on myService.makeRemoteCall(), because myService.myOtherService is just an empty object at this point (its value was never updated by angular) – Georgii Oleinikov May 16 '14 at 22:16
  • 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. – twDuke Aug 28 '15 at 12:19

8 Answers8

176

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;   
    };
  }
sarin
  • 5,227
  • 3
  • 34
  • 63
dnc253
  • 39,967
  • 41
  • 141
  • 157
  • 1
    And you killed me yesterday by not showing up in results. Beautiful display of andCallFake(). Thank you. – Priya Ranjan Singh Nov 04 '14 at 04:32
  • 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. – Jordan Running Nov 07 '14 at 18:51
  • 1
    How would you call `$digest` in this case when you don't have access to the scope? – Jim Aho May 18 '15 at 15:33
  • 7
    @JimAho Typically you just inject `$rootScope` and call `$digest` on that. – dnc253 May 18 '15 at 15:39
  • @dnc253 Yes thanks! I just found that out on this page: https://docs.angularjs.org/api/ng/service/$q. I totally had missed the integration of $q with the angular scoping mechanisms. – Jim Aho May 18 '15 at 15:49
  • 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. – Mark Nadig Sep 01 '15 at 16:29
  • $digest is most commonly forgotten step. – ATrubka Jun 09 '16 at 20:58
  • 1
    Using deferred in this case is unnecessary. You can just use `$q.when()` http://www.codelord.net/2015/09/24/$q-dot-defer-youre-doing-it-wrong/ – fodma1 Oct 31 '16 at 10:31
  • @dnc253: I have an issue to get data from controller can you please look into it. Can you help me on it. https://stackoverflow.com/questions/53629138/how-to-test-and-resolve-controller-data-then-function-promise-and-get-orgin – Ajay.k Dec 06 '18 at 14:01
70

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)

Priya Ranjan Singh
  • 1,567
  • 1
  • 15
  • 29
  • 12
    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. – ccnokes Feb 04 '15 at 19:30
14
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();  
    });
});
Darren Corbett
  • 1,296
  • 1
  • 9
  • 6
  • 1
    This is how I have done it in the past. Create a spy that returns a fake that mimics the "then" – Darren Corbett Apr 15 '15 at 11:56
  • 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 ! – Rob Paddock Apr 15 '15 at 23:35
  • 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. – Darren Corbett Apr 28 '15 at 07:19
  • 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 https://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 Sep 01 '15 at 16:27
  • This post http://ng-learn.org/2014/08/Testing_Promises_with_Jasmine_Provide_Spy/ describes the usage of fake "then" throughly. – Custodio Sep 09 '15 at 18:10
8

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);


    });
Mike Lunn
  • 2,310
  • 3
  • 21
  • 24
  • 2
    this is the missing piece, calling `$rootScope.$digest()` to get the promise to be resolved –  May 07 '17 at 19:35
2

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 })
);
MHX
  • 1,581
  • 2
  • 21
  • 31
Abdennour TOUMI
  • 87,526
  • 38
  • 249
  • 254
0

Honestly.. you are going about this the wrong way by relying on inject to mock a service instead of module. Also, calling inject in a beforeEach is an anti-pattern as it makes mocking difficult on a per test basis.

Here is how I would do this...

module(function ($provide) {
  // By using a decorator we can access $q and stub our method with a promise.
  $provide.decorator('myOtherService', function ($delegate, $q) {

    $delegate.makeRemoteCallReturningPromise = function () {
      var dfd = $q.defer();
      dfd.resolve('some value');
      return dfd.promise;
    };
  });
});

Now when you inject your service it will have a properly mocked method for usage.

  • 3
    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. – Darren Corbett Jun 04 '15 at 18:54
0

I found that useful, stabbing service function as sinon.stub().returns($q.when({})):

this.myService = {
   myFunction: sinon.stub().returns( $q.when( {} ) )
};

this.scope = $rootScope.$new();
this.angularStubs = {
    myService: this.myService,
    $scope: this.scope
};
this.ctrl = $controller( require( 'app/bla/bla.controller' ), this.angularStubs );

controller:

this.someMethod = function(someObj) {
   myService.myFunction( someObj ).then( function() {
        someObj.loaded = 'bla-bla';
   }, function() {
        // failure
   } );   
};

and test

const obj = {
    field: 'value'
};
this.ctrl.someMethod( obj );

this.scope.$digest();

expect( this.myService.myFunction ).toHaveBeenCalled();
expect( obj.loaded ).toEqual( 'bla-bla' );
Dmitri Algazin
  • 3,332
  • 27
  • 30
-1

The code snippet:

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

Can be written in a more concise form:

spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue(function() {
    return $q.resolve('Remote call result');
});
trunikov
  • 97
  • 1
  • 4