【问题标题】:How do I mock a service that returns promise in AngularJS Jasmine unit test?如何模拟在 AngularJS Jasmine 单元测试中返回承诺的服务?
【发布时间】:2014-07-05 11:36:08
【问题描述】:

我有myService 使用myOtherService,它进行远程调用,返回承诺:

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

      return {
        makeRemoteCall: makeRemoteCall
      };      
    }
  ])

要对myService 进行单元测试,我需要模拟myOtherService,使其makeRemoteCallReturningPromise 方法返回一个promise。我就是这样做的:

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

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

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

【问题讨论】:

  • myService.makeRemoteCall() 上真的有错误吗?如果是这样,问题在于myService 没有makeRemoteCall,与您的嘲笑myOtherService 无关。
  • 错误出现在 myService.makeRemoteCall() 上,因为此时 myService.myOtherService 只是一个空对象(其值从未被 angular 更新过)
  • 您将空对象添加到 ioc 容器中,然后更改引用 myOtherServiceMock 以指向您监视的新对象。 ioc 容器中的内容不会反映这一点,因为引用已更改。

标签: javascript angularjs unit-testing mocking jasmine


【解决方案1】:

代码sn-p:

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

【讨论】:

    【解决方案2】:

    我发现有用的刺伤服务函数为 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 );
    

    控制器:

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

    【讨论】:

      【解决方案3】:

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

      【讨论】:

        【解决方案4】:

        我不确定为什么你这样做的方式不起作用,但我通常使用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 的测试部分。

        -----编辑------

        仔细查看您的操作后,我认为您的代码中存在问题。在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;   
            };
          }
        

        【讨论】:

        • 你昨天因为没有出现在结果中而害死了我。 andCallFake() 的漂亮展示。谢谢。
        • 您可以使用andReturnValue(deferred.promise)(或Jasmine 2.0+ 中的and.returnValue(deferred.promise))代替andCallFake。当然,您需要在调用spyOn 之前定义deferred。
        • 在这种情况下,如果您无法访问范围,您将如何调用$digest?
        • @JimAho 通常你只需注入$rootScope 并在上面调用$digest。
        • 在这种情况下使用 deferred 是不必要的。你可以用$q.when()codelord.net/2015/09/24/$q-dot-defer-youre-doing-it-wrong
        【解决方案5】:

        我们也可以直接spy写jasmine返回promise的实现。

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

        茉莉花2:

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

        (抄自 cmets,感谢 ccnokes)

        【讨论】:

        • 使用 Jasmine 2.0 的用户请注意,.andReturn() 已被 .and.returnValue 取代。所以上面的例子是:spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue($q.when({}));我刚刚花了半个小时才弄清楚。
        【解决方案6】:
        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();  
            });
        });
        

        【讨论】:

        • 这就是我过去的做法。创建一个间谍,返回一个模仿“then”的假货
        • 您能否提供一个完整测试的示例。我有一个类似的问题,即有一个返回承诺的服务,但其中也有一个返回承诺的调用!
        • 嗨 Rob,不知道为什么要模拟一个模拟对另一个服务的调用,当然你想在测试该函数时对其进行测试。如果您正在模拟的函数调用调用服务获取数据然后影响该数据,您的模拟承诺将返回一个虚假的受影响数据集,至少我会这样做。
        • 我从这条路开始,它适用于简单的场景。我什至创建了一个模拟链接并提供“保持”/“中断”帮助器来调用链 gist.github.com/marknadig/c3e8f2d3fff9d22da42b 的模拟,但是在更复杂的场景中,这会失败。就我而言,我有一项服务可以有条件地从缓存中返回项目(带延迟)或发出请求。所以,它正在创造自己的承诺。
        • 这篇帖子ng-learn.org/2014/08/Testing_Promises_with_Jasmine_Provide_Spy 彻底描述了假“then”的用法。
        【解决方案7】:

        老实说..您通过依赖注入来模拟服务而不是模块来以错误的方式解决这个问题。此外,在 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;
            };
          });
        });
        

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

        【讨论】:

        • a before each 的全部意义在于它在每次测试之前被调用我不知道你是如何编写测试的,但我个人为一个函数编写多个测试,因此我会有一个共同的将在每次测试之前调用的基本设置。此外,您可能想查找与软件工程相关的反模式的理解含义。
        【解决方案8】:

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

        【讨论】:

        • 这是缺失的部分,调用$rootScope.$digest() 以获得解决的承诺
        猜你喜欢
        • 2014-05-19
        • 2014-06-17
        • 2014-05-14
        • 2015-07-12
        • 2016-06-17
        • 1970-01-01
        • 2014-03-23
        • 2017-08-02
        • 1970-01-01
        相关资源
        最近更新 更多