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

IT技术 javascript angularjs unit-testing mocking jasmine
2021-01-27 21:14:07

我有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方法就会返回一个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');
      });    
  }));  

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

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

6个回答

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

- - - 编辑 - - -

在仔细查看您在做什么之后,我想我在您的代码中看到了问题。在 中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() 的漂亮展示。谢谢你。
2021-03-21 21:14:07
$digest在这种情况下,如果您无权访问范围,您将如何调用
2021-03-28 21:14:07
而不是andCallFake您可以使用andReturnValue(deferred.promise)(或and.returnValue(deferred.promise)在 Jasmine 2.0+ 中)。当然,您需要deferred在调用之前定义spyOn
2021-04-08 21:14:07
在这种情况下使用 deferred 是不必要的。你可以只使用$q.when() codelord.net/2015/09/24/$q-dot-defer-youre-doing-it-wrong
2021-04-10 21:14:07
@JimAho 通常,您只需注入$rootScope并调用$digest它。
2021-04-12 21:14:07

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

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

对于茉莉花 2:

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

(从评论中复制,感谢ccnokes)

使用 Jasmine 2.0 的人请注意,.andReturn() 已替换为 .and.returnValue。所以上面的例子是:spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue($q.when({}));我只是花了半个小时才弄清楚。
2021-03-21 21:14:07
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();  
    });
});
您能否提供一个完整测试的示例。我有一个类似的问题,即有一个返回Promise的服务,但在其中也进行了一个返回Promise的调用!
2021-04-02 21:14:07
我开始走这条路,它适用于简单的场景。我什至创建了一个模拟链接并提供“保持”/“中断”帮助器来调用链gist.github.com/marknadig/c3e8f2d3fff9d22da42b 在更复杂的场景中,这会失败。就我而言,我有一个服务可以有条件地从缓存中返回项目(带延迟)或发出请求。所以,它正在创造它自己的Promise。
2021-04-05 21:14:07
这就是我过去所做的。创建一个间谍,返回一个模仿“then”的假货
2021-04-05 21:14:07
嗨,Rob,不确定为什么要模拟模拟对另一个服务进行的调用,您肯定想在测试该功能时对其进行测试。如果您正在模拟的函数调用调用服务获取数据然后影响该数据,您的模拟Promise将返回一个假的受影响的数据集,至少我会这样做。
2021-04-07 21:14:07
这篇文章ng-learn.org/2014/08/Testing_Promises_with_Jasmine_Provide_Spy 彻底描述了假“then”的用法。
2021-04-12 21:14:07

您可以使用像 sinon 这样的存根库来模拟您的服务。然后你可以返回 $q.when() 作为你的Promise。如果范围对象的值来自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()得到解决的Promise
2021-04-12 21:14:07

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