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

Posted

技术标签:

【中文标题】如何模拟在 AngularJS Jasmine 单元测试中返回承诺的服务?【英文标题】:How do I mock a service that returns promise in AngularJS Jasmine unit test? 【发布时间】: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 容器中的内容不会反映这一点,因为引用已更改。 【参考方案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() 以获得解决的承诺

以上是关于如何模拟在 AngularJS Jasmine 单元测试中返回承诺的服务?的主要内容,如果未能解决你的问题,请参考以下文章

在 AngularJS/Jasmine 中注入 Mock 服务

基于karma和jasmine的Angularjs 单元测试

Angularjs 基于karma和jasmine的单元测试

AngularJS – 如何在 Jasmine 中为输入事件指令编写单元测试

在Visual Studio 2013中使用Jasmine + Karma进行AngularJS测试

Karma + Jasmine 下 angular.mock.inject 的 TypeScript AngularJS 控制器测试问题