ChatGPT解决这个技术问题 Extra ChatGPT

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

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?

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.
The error is on myService.makeRemoteCall(), because myService.myOtherService is just an empty object at this point (its value was never updated by angular)
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.

s
sarin

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

And you killed me yesterday by not showing up in results. Beautiful display of andCallFake(). Thank you.
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.
How would you call $digest in this case when you don't have access to the scope?
@JimAho Typically you just inject $rootScope and call $digest on that.
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
P
Priya Ranjan Singh

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)


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.
D
Darren Corbett
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();  
    });
});

This is how I have done it in the past. Create a spy that returns a fake that mimics the "then"
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 !
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.
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.
This post ng-learn.org/2014/08/Testing_Promises_with_Jasmine_Provide_Spy describes the usage of fake "then" throughly.
M
Mike Lunn

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


    });

this is the missing piece, calling $rootScope.$digest() to get the promise to be resolved
M
MHX

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

K
Keith Loy

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.


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.
D
Dmitri Algazin

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

t
trunikov

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