如何在Angularjs Jasmine单元测试中模拟返回承诺的服务?

时间:2014-05-16 21:47:06

标签: javascript angularjs unit-testing mocking jasmine

我的myService使用myOtherService,它进行远程调用,返回promise:

angular.module('app.myService', ['app.myOtherService'])
  .factory('myService', [myOtherService,

    function(myOtherService) {
      function makeRemoteCall() {
        return myOtherService.makeRemoteCallReturningPromise();
      }

      return {
        makeRemoteCall: makeRemoteCall
      };      
    }
  ])

要对myService进行单元测试,我需要模拟myOtherService,以便其makeRemoteCallReturningPromise()方法返回一个承诺。我就是这样做的:

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

从上面可以看出,我的模拟定义取决于$q,我必须使用inject()加载。{1}}。此外,注入模拟应该在module()中进行,这应该在之前发生 inject()。但是,一旦我更改了mock的值,它就不会更新。

这样做的正确方法是什么?

8 个答案:

答案 0 :(得分:172)

我不确定为什么你这样做的方式不起作用,但我通常使用spyOn功能。像这样:

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

另请注意,您需要调用$digest来调用then函数。请参阅$q documentation测试部分。

<强> ------ ------ EDIT

仔细观察您正在做的事情之后,我想我在您的代码中看到了问题。在beforeEach中,您将myOtherServiceMock设置为一个全新的对象。 $provide永远不会看到此引用。您只需更新现有参考:

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

答案 1 :(得分:69)

我们也可以直接通过间谍编写茉莉花实现的回复承诺。

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

对于Jasmine 2:

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

(复制自评论,感谢ccnokes)

答案 2 :(得分:12)

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

答案 3 :(得分:8)

您可以使用像sinon这样的存根库来模拟您的服务。然后您可以返回$ q.when()作为您的承诺。如果您的范围对象的值来自承诺结果,则需要调用范围。$ 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);


    });

答案 4 :(得分:2)

使用sinon

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

已知httpPromise可以是:

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

答案 5 :(得分:0)

老实说..你依靠注入来模拟服务而不是模块,这是错误的。另外,在beforeEach中调用inject是一种反模式,因为它会使每次测试都难以进行模拟。

我将如何做到这一点...

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

现在,当您注入服务时,它将有一个正确模拟的使用方法。

答案 6 :(得分:0)

我发现有用的刺穿服务函数为sinon.stub()。return($ 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 );

控制器:

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

并测试

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

this.scope.$digest();

expect( this.myService.myFunction ).toHaveBeenCalled();
expect( obj.loaded ).toEqual( 'bla-bla' );

答案 7 :(得分:-1)

代码段:

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

可以用更简洁的形式编写:

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